From 7ee008e150bc5bcf76082d726f719ee0fdfda982 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 11 Feb 2026 02:37:39 -0600 Subject: Fluorine Manager: full Linux port of Mod Organizer 2 Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 193 + .github/workflows/docker.yml | 45 + .gitignore | 28 + CMakeLists.txt | 204 + LICENSE.txt | 21 + README.md | 48 + build.sh | 27 + docker/AppRun.sh | 125 + docker/Dockerfile | 98 + docker/build-inner.sh | 296 + docs/FAQ.md | 26 + flatpak/com.fluorine.manager.desktop | 22 + flatpak/com.fluorine.manager.metainfo.xml | 38 + flatpak/com.fluorine.manager.png | Bin 0 -> 51022 bytes flatpak/com.fluorine.manager.yml | 364 + flatpak/flatpak-install.sh | 58 + flatpak/fluorine-manager-wrapper.sh | 109 + libs/7zip/CMakeLists.txt | 454 + libs/archive/.clang-format | 41 + libs/archive/.git-blame-ignore-revs | 1 + libs/archive/.gitattributes | 7 + libs/archive/.github/workflows/build.yml | 39 + libs/archive/.github/workflows/linting.yml | 16 + libs/archive/.gitignore | 5 + libs/archive/.pre-commit-config.yaml | 20 + libs/archive/CMakeLists.txt | 37 + libs/archive/CMakePresets.json | 74 + libs/archive/LICENSE | 165 + libs/archive/README.md | 239 + libs/archive/cmake/config.cmake.in | 7 + libs/archive/include/archive/archive.h | 280 + libs/archive/src/CMakeLists.txt | 101 + libs/archive/src/archive.cpp | 609 ++ libs/archive/src/compat.h | 87 + libs/archive/src/extractcallback.cpp | 364 + libs/archive/src/extractcallback.h | 133 + libs/archive/src/fileio.cpp | 449 + libs/archive/src/fileio.h | 343 + libs/archive/src/formatter.h | 121 + libs/archive/src/inputstream.cpp | 70 + libs/archive/src/inputstream.h | 54 + libs/archive/src/instrument.h | 111 + libs/archive/src/interfaceguids.cpp | 37 + libs/archive/src/library.h | 164 + libs/archive/src/multioutputstream.cpp | 142 + libs/archive/src/multioutputstream.h | 106 + libs/archive/src/opencallback.cpp | 168 + libs/archive/src/opencallback.h | 75 + libs/archive/src/propertyvariant.cpp | 214 + libs/archive/src/propertyvariant.h | 56 + libs/archive/src/unknown_impl.h | 191 + libs/archive/src/version.rc | 28 + libs/archive/thirdparty/C/7zTypes.h | 597 ++ libs/archive/thirdparty/C/7zWindows.h | 101 + libs/archive/thirdparty/C/Compiler.h | 236 + libs/archive/thirdparty/C/Precomp.h | 127 + .../archive/thirdparty/CPP/7zip/Archive/IArchive.h | 754 ++ libs/archive/thirdparty/CPP/7zip/ICoder.h | 477 + libs/archive/thirdparty/CPP/7zip/IDecl.h | 76 + libs/archive/thirdparty/CPP/7zip/IPassword.h | 54 + libs/archive/thirdparty/CPP/7zip/IProgress.h | 20 + libs/archive/thirdparty/CPP/7zip/IStream.h | 210 + libs/archive/thirdparty/CPP/7zip/MyVersion.h | 2 + libs/archive/thirdparty/CPP/7zip/PropID.h | 178 + libs/archive/thirdparty/CPP/Common/Common.h | 28 + libs/archive/thirdparty/CPP/Common/Common0.h | 330 + libs/archive/thirdparty/CPP/Common/MyGuidDef.h | 63 + libs/archive/thirdparty/CPP/Common/MyTypes.h | 38 + libs/archive/thirdparty/CPP/Common/MyUnknown.h | 8 + libs/archive/thirdparty/CPP/Common/MyWindows.cpp | 292 + libs/archive/thirdparty/CPP/Common/MyWindows.h | 325 + libs/archive/thirdparty/CPP/Common/NewHandler.h | 121 + libs/archive/thirdparty/CPP/Common/StdAfx.h | 8 + libs/archive/vcpkg.json | 10 + libs/basic_games/.github/workflows/build.yml | 44 + libs/basic_games/.github/workflows/linters.yml | 24 + libs/basic_games/.gitignore | 12 + libs/basic_games/.pre-commit-config.yaml | 25 + libs/basic_games/CMakeLists.txt | 8 + libs/basic_games/CMakePresets.json | 17 + libs/basic_games/LICENSE | 7 + libs/basic_games/README.md | 233 + libs/basic_games/__init__.py | 96 + libs/basic_games/basic_features/__init__.py | 10 + .../basic_features/basic_local_savegames.py | 27 + .../basic_features/basic_mod_data_checker.py | 226 + .../basic_features/basic_save_game_info.py | 226 + libs/basic_games/basic_features/utils.py | 7 + libs/basic_games/basic_game.py | 668 ++ libs/basic_games/basic_game_ini.py | 23 + libs/basic_games/eadesktop_utils.py | 85 + libs/basic_games/epic_utils.py | 108 + libs/basic_games/games/__init__.py | 0 libs/basic_games/games/baldursgate3/__init__.py | 0 .../games/baldursgate3/bg3_data_checker.py | 58 + .../games/baldursgate3/bg3_data_content.py | 54 + .../games/baldursgate3/bg3_file_mapper.py | 131 + libs/basic_games/games/baldursgate3/bg3_utils.py | 274 + .../games/baldursgate3/lslib_retriever.py | 162 + libs/basic_games/games/baldursgate3/pak_parser.py | 295 + .../games/baldursgate3/plugins/__init__.py | 13 + .../games/baldursgate3/plugins/bg3_tool_plugin.py | 49 + .../plugins/check_for_lslib_updates_plugin.py | 15 + .../plugins/convert_jsons_to_yaml_plugin.py | 58 + .../games/baldursgate3/plugins/icons.py | 42 + .../plugins/reparse_pak_metadata_plugin.py | 17 + libs/basic_games/games/game_arkhamcity.py | 87 + libs/basic_games/games/game_assettocorsa.py | 18 + libs/basic_games/games/game_baldursgate3.py | 239 + libs/basic_games/games/game_blackandwhite2.py | 341 + libs/basic_games/games/game_bladeandsorcery.py | 98 + libs/basic_games/games/game_borderlands1.py | 153 + libs/basic_games/games/game_control.py | 63 + libs/basic_games/games/game_cyberpunk2077.py | 798 ++ libs/basic_games/games/game_da2.py | 35 + libs/basic_games/games/game_daggerfallunity.py | 56 + libs/basic_games/games/game_dao.py | 31 + libs/basic_games/games/game_darkestdungeon.py | 245 + .../games/game_darkmessiahofmightandmagic.py | 48 + libs/basic_games/games/game_darksouls.py | 22 + libs/basic_games/games/game_darksouls2sotfs.py | 22 + libs/basic_games/games/game_dispatch.py | 102 + libs/basic_games/games/game_divinityoriginalsin.py | 39 + .../games/game_divinityoriginalsinee.py | 119 + .../games/game_dragonsdogmadarkarisen.py | 19 + libs/basic_games/games/game_dungeonsiege1.py | 92 + libs/basic_games/games/game_dungeonsiege2.py | 92 + libs/basic_games/games/game_f123.py | 13 + libs/basic_games/games/game_fantasylifei.py | 102 + .../basic_games/games/game_finalfantasy7rebirth.py | 157 + libs/basic_games/games/game_finalfantasy7remake.py | 78 + libs/basic_games/games/game_gta-3-de.py | 72 + libs/basic_games/games/game_gta-san-andreas-de.py | 72 + libs/basic_games/games/game_gta-vice-city-de.py | 72 + libs/basic_games/games/game_kerbalspaceprogram.py | 61 + .../games/game_kingdomcomedeliverance.py | 48 + libs/basic_games/games/game_masterduel.py | 89 + libs/basic_games/games/game_metalgearsolid2mc.py | 17 + libs/basic_games/games/game_metalgearsolid3mc.py | 15 + libs/basic_games/games/game_mirrorsedge.py | 19 + libs/basic_games/games/game_monsterhunterrise.py | 15 + libs/basic_games/games/game_monsterhunterworld.py | 21 + libs/basic_games/games/game_mountandblade2.py | 100 + libs/basic_games/games/game_msfs2020.py | 34 + libs/basic_games/games/game_nfshs.py | 28 + libs/basic_games/games/game_nierautomata.py | 14 + libs/basic_games/games/game_nomanssky.py | 38 + libs/basic_games/games/game_oblivion_remaster.py | 441 + libs/basic_games/games/game_readyornot.py | 24 + libs/basic_games/games/game_schedule1.py | 112 + .../games/game_sekiroshadowsdietwice.py | 14 + libs/basic_games/games/game_silenthill2remake.py | 113 + libs/basic_games/games/game_silksong.py | 121 + libs/basic_games/games/game_sims4.py | 153 + libs/basic_games/games/game_stalkeranomaly.py | 293 + libs/basic_games/games/game_stardewvalley.py | 57 + libs/basic_games/games/game_starsector.py | 30 + .../games/game_starwars-empire-at-war-foc.py | 33 + .../games/game_starwars-empire-at-war.py | 33 + .../games/game_subnautica-below-zero.py | 32 + libs/basic_games/games/game_subnautica.py | 279 + libs/basic_games/games/game_tdu.py | 36 + libs/basic_games/games/game_tdu2.py | 35 + .../games/game_thebindingofisaacrebirth.py | 23 + libs/basic_games/games/game_thps3.py | 38 + libs/basic_games/games/game_thps4.py | 40 + libs/basic_games/games/game_thug.py | 36 + libs/basic_games/games/game_thug2.py | 54 + libs/basic_games/games/game_tmuf.py | 35 + libs/basic_games/games/game_trainsimulator.py | 36 + libs/basic_games/games/game_valheim.py | 427 + libs/basic_games/games/game_valkyriachronicles.py | 15 + libs/basic_games/games/game_vampirebloodlines.py | 103 + libs/basic_games/games/game_witcher1.py | 93 + libs/basic_games/games/game_witcher2.py | 67 + libs/basic_games/games/game_witcher3.py | 52 + libs/basic_games/games/game_x4.py | 17 + libs/basic_games/games/game_xplane11.py | 16 + libs/basic_games/games/game_zeusandposeidon.py | 70 + .../games/oblivion_remaster/__init__.py | 0 .../games/oblivion_remaster/constants.py | 14 + .../games/oblivion_remaster/game_plugins.py | 207 + .../games/oblivion_remaster/mod_data_checker.py | 451 + .../games/oblivion_remaster/mod_data_content.py | 109 + .../games/oblivion_remaster/paks/__init__.py | 0 .../games/oblivion_remaster/paks/model.py | 253 + .../games/oblivion_remaster/paks/view.py | 33 + .../games/oblivion_remaster/paks/widget.py | 213 + .../games/oblivion_remaster/script_extender.py | 37 + .../games/oblivion_remaster/ue4ss/__init__.py | 0 .../games/oblivion_remaster/ue4ss/model.py | 125 + .../games/oblivion_remaster/ue4ss/view.py | 31 + .../games/oblivion_remaster/ue4ss/widget.py | 175 + .../games/quarantine/game_masseffectlegendary.py | 24 + libs/basic_games/games/quarantine/readme.txt | 3 + libs/basic_games/games/stalkeranomaly/XRIO.py | 148 + libs/basic_games/games/stalkeranomaly/XRMath.py | 37 + libs/basic_games/games/stalkeranomaly/XRNET.py | 45 + libs/basic_games/games/stalkeranomaly/XRObject.py | 277 + libs/basic_games/games/stalkeranomaly/XRSave.py | 164 + libs/basic_games/games/stalkeranomaly/__init__.py | 38 + libs/basic_games/gog_utils.py | 35 + libs/basic_games/origin_utils.py | 104 + libs/basic_games/plugin-requirements.txt | 4 + libs/basic_games/poetry.lock | 337 + libs/basic_games/pyproject.toml | 59 + libs/basic_games/steam_utils.py | 187 + libs/basic_games/vcpkg.json | 17 + libs/basic_games/winreg.py | 14 + libs/bsa_extractor/.gitattributes | 7 + libs/bsa_extractor/.github/workflows/build.yml | 20 + libs/bsa_extractor/.gitignore | 5 + libs/bsa_extractor/.pre-commit-config.yaml | 20 + libs/bsa_extractor/CMakeLists.txt | 5 + libs/bsa_extractor/CMakePresets.json | 52 + libs/bsa_extractor/src/CMakeLists.txt | 13 + libs/bsa_extractor/src/bsa_extractor_en.ts | 59 + libs/bsa_extractor/src/bsaextractor.cpp | 140 + libs/bsa_extractor/src/bsaextractor.h | 34 + libs/bsa_extractor/vcpkg.json | 36 + libs/bsa_ffi/CMakeLists.txt | 34 + libs/bsa_ffi/Cargo.lock | 520 + libs/bsa_ffi/Cargo.toml | 18 + libs/bsa_ffi/include/bsa_ffi.h | 54 + libs/bsa_ffi/src/archive/ba2_reader.rs | 125 + libs/bsa_ffi/src/archive/ba2_writer.rs | 336 + libs/bsa_ffi/src/archive/mod.rs | 436 + libs/bsa_ffi/src/archive/reader.rs | 154 + libs/bsa_ffi/src/archive/tes3_reader.rs | 104 + libs/bsa_ffi/src/archive/writer.rs | 256 + libs/bsa_ffi/src/lib.rs | 325 + libs/bsapacker/.gitattributes | 7 + libs/bsapacker/.github/workflows/build.yml | 20 + libs/bsapacker/.gitignore | 8 + libs/bsapacker/.pre-commit-config.yaml | 20 + libs/bsapacker/CMakeLists.txt | 13 + libs/bsapacker/CMakePresets.json | 63 + libs/bsapacker/LICENSE.md | 21 + libs/bsapacker/README.md | 2 + libs/bsapacker/src/ArchiveAutoService.cpp | 102 + libs/bsapacker/src/ArchiveBuildDirector.cpp | 28 + libs/bsapacker/src/ArchiveBuilderFactory.cpp | 59 + libs/bsapacker/src/ArchiveBuilderHelper.cpp | 77 + libs/bsapacker/src/ArchiveNameService.cpp | 75 + libs/bsapacker/src/ArchiveNameService.h | 24 + libs/bsapacker/src/BSArchive.cpp | 184 + libs/bsapacker/src/BSArchiveAuto.cpp | 101 + libs/bsapacker/src/BSArchiveEntries.cpp | 65 + libs/bsapacker/src/BsaPacker.cpp | 99 + libs/bsapacker/src/BsaPacker.h | 42 + libs/bsapacker/src/BsaPackerWorker.cpp | 66 + libs/bsapacker/src/BsaPackerWorker.h | 42 + libs/bsapacker/src/CMakeLists.txt | 36 + libs/bsapacker/src/DummyPluginLogic.cpp | 58 + libs/bsapacker/src/DummyPluginLogic.h | 30 + libs/bsapacker/src/DummyPluginServiceFactory.cpp | 54 + libs/bsapacker/src/DummyPluginServiceFactory.h | 29 + libs/bsapacker/src/Fallout3DummyPluginService.cpp | 26 + libs/bsapacker/src/Fallout3DummyPluginService.h | 41 + libs/bsapacker/src/Fallout4DummyPluginService.cpp | 26 + libs/bsapacker/src/Fallout4DummyPluginService.h | 34 + libs/bsapacker/src/FileWriterService.cpp | 18 + libs/bsapacker/src/FileWriterService.h | 16 + libs/bsapacker/src/GeneralArchiveBuilder.cpp | 97 + libs/bsapacker/src/HideLooseAssetService.cpp | 69 + libs/bsapacker/src/HideLooseAssetService.h | 23 + libs/bsapacker/src/ModContext.cpp | 66 + libs/bsapacker/src/ModContext.h | 25 + libs/bsapacker/src/ModDto.cpp | 43 + libs/bsapacker/src/ModDto.h | 29 + libs/bsapacker/src/ModDtoFactory.cpp | 90 + libs/bsapacker/src/NewVegasDummyPluginService.cpp | 28 + libs/bsapacker/src/NewVegasDummyPluginService.h | 42 + libs/bsapacker/src/NexusId.h | 22 + libs/bsapacker/src/NullArchiveBuilder.cpp | 32 + libs/bsapacker/src/NullDummyPluginService.cpp | 9 + libs/bsapacker/src/NullDummyPluginService.h | 17 + libs/bsapacker/src/NullModDto.cpp | 29 + libs/bsapacker/src/NullModDto.h | 20 + libs/bsapacker/src/OblivionDummyPluginService.cpp | 26 + libs/bsapacker/src/OblivionDummyPluginService.h | 34 + libs/bsapacker/src/OverrideFileService.cpp | 44 + libs/bsapacker/src/OverrideFileService.h | 21 + libs/bsapacker/src/PackerDialog.cpp | 98 + libs/bsapacker/src/PackerDialog.h | 48 + libs/bsapacker/src/PackerDialogFactory.cpp | 5 + libs/bsapacker/src/PackerDialogFactory.h | 16 + libs/bsapacker/src/SettingsService.cpp | 28 + libs/bsapacker/src/SettingsService.h | 32 + libs/bsapacker/src/SkyrimDummyPluginService.cpp | 26 + libs/bsapacker/src/SkyrimDummyPluginService.h | 34 + libs/bsapacker/src/SkyrimSEDummyPluginService.cpp | 26 + libs/bsapacker/src/SkyrimSEDummyPluginService.h | 35 + libs/bsapacker/src/StarfieldDummyPluginService.cpp | 26 + libs/bsapacker/src/StarfieldDummyPluginService.h | 34 + libs/bsapacker/src/TextureArchiveBuilder.cpp | 137 + libs/bsapacker/src/TexturelessArchiveBuilder.cpp | 98 + libs/bsapacker/src/bsa_packer.json | 1 + libs/bsapacker/src/bsa_packer_en.ts | 87 + libs/bsapacker/src/bsapacker/ArchiveAutoService.h | 19 + .../bsapacker/src/bsapacker/ArchiveBuildDirector.h | 27 + .../src/bsapacker/ArchiveBuilderFactory.h | 31 + .../bsapacker/src/bsapacker/ArchiveBuilderHelper.h | 29 + .../src/bsapacker/GeneralArchiveBuilder.h | 37 + libs/bsapacker/src/bsapacker/IArchiveAutoService.h | 18 + libs/bsapacker/src/bsapacker/IArchiveBuilder.h | 24 + .../src/bsapacker/IArchiveBuilderFactory.h | 18 + .../src/bsapacker/IArchiveBuilderHelper.h | 22 + libs/bsapacker/src/bsapacker/IArchiveNameService.h | 21 + libs/bsapacker/src/bsapacker/IDummyPluginLogic.h | 19 + libs/bsapacker/src/bsapacker/IDummyPluginService.h | 17 + .../src/bsapacker/IDummyPluginServiceFactory.h | 17 + libs/bsapacker/src/bsapacker/IEmitsValueChanged.h | 25 + libs/bsapacker/src/bsapacker/IFileWriterService.h | 16 + .../src/bsapacker/IHideLooseAssetService.h | 16 + libs/bsapacker/src/bsapacker/IModContext.h | 20 + libs/bsapacker/src/bsapacker/IModDto.h | 20 + libs/bsapacker/src/bsapacker/IModDtoFactory.h | 17 + .../bsapacker/src/bsapacker/IOverrideFileService.h | 16 + libs/bsapacker/src/bsapacker/IPackerDialog.h | 25 + .../bsapacker/src/bsapacker/IPackerDialogFactory.h | 16 + libs/bsapacker/src/bsapacker/ISettingsService.h | 17 + libs/bsapacker/src/bsapacker/ModDtoFactory.h | 33 + libs/bsapacker/src/bsapacker/NullArchiveBuilder.h | 28 + .../src/bsapacker/TextureArchiveBuilder.h | 41 + .../src/bsapacker/TexturelessArchiveBuilder.h | 39 + libs/bsapacker/src/bsapacker_global.h | 7 + libs/bsapacker/src/qlibbsarch/BSArchive.h | 38 + libs/bsapacker/src/qlibbsarch/BSArchiveAuto.h | 91 + libs/bsapacker/src/qlibbsarch/BSArchiveEntries.h | 23 + libs/bsapacker/src/qlibbsarch/QLibbsarch.h | 42 + libs/bsapacker/tests/ArchiveAutoServiceFacts.cpp | 26 + .../bsapacker/tests/ArchiveBuilderFactoryFacts.cpp | 242 + libs/bsapacker/tests/ArchiveBuilderHelperFacts.cpp | 37 + libs/bsapacker/tests/ArchiveNameServiceFacts.cpp | 260 + libs/bsapacker/tests/BsaPackerFacts.cpp | 34 + libs/bsapacker/tests/BsaPackerWorkerFacts.cpp | 67 + libs/bsapacker/tests/CMakeLists.txt | 5 + .../tests/DummyPluginServiceFactoryFacts.cpp | 44 + .../tests/Fallout3DummyPluginServiceFacts.cpp | 39 + .../tests/Fallout4DummyPluginServiceFacts.cpp | 39 + libs/bsapacker/tests/FileWriterServiceFacts.cpp | 26 + .../bsapacker/tests/GeneralArchiveBuilderFacts.cpp | 35 + .../bsapacker/tests/HideLooseAssetServiceFacts.cpp | 33 + libs/bsapacker/tests/MockArchiveAutoService.h | 10 + libs/bsapacker/tests/MockArchiveBuilderFactory.h | 11 + libs/bsapacker/tests/MockArchiveBuilderHelper.h | 15 + libs/bsapacker/tests/MockArchiveNameService.h | 13 + libs/bsapacker/tests/MockDummyPluginLogic.h | 11 + .../tests/MockDummyPluginServiceFactory.h | 10 + libs/bsapacker/tests/MockFileWriterService.h | 10 + libs/bsapacker/tests/MockHideLooseAssetService.h | 10 + libs/bsapacker/tests/MockModContext.h | 13 + libs/bsapacker/tests/MockModDtoFactory.h | 10 + libs/bsapacker/tests/MockOrganizer.h | 61 + libs/bsapacker/tests/MockOverrideFileService.h | 10 + libs/bsapacker/tests/MockPackerDialog.h | 17 + libs/bsapacker/tests/MockSettingsService.h | 10 + libs/bsapacker/tests/ModContextFacts.cpp | 33 + libs/bsapacker/tests/ModDtoFactoryFacts.cpp | 53 + .../tests/NewVegasDummyPluginServiceFacts.cpp | 39 + .../tests/OblivionDummyPluginServiceFacts.cpp | 39 + .../tests/SkyrimDummyPluginServiceFacts.cpp | 39 + .../tests/SkyrimSEDummyPluginServiceFacts.cpp | 39 + .../bsapacker/tests/TextureArchiveBuilderFacts.cpp | 35 + .../tests/TexturelessArchiveBuilderFacts.cpp | 35 + libs/bsapacker/vcpkg.json | 34 + libs/bsatk/.clang-format | 41 + libs/bsatk/.gitattributes | 7 + libs/bsatk/.github/workflows/build.yml | 40 + libs/bsatk/.github/workflows/linting.yml | 16 + libs/bsatk/.gitignore | 5 + libs/bsatk/.pre-commit-config.yaml | 20 + libs/bsatk/CMakeFiles/CMakeSystem.cmake | 15 + libs/bsatk/CMakeLists.txt | 30 + libs/bsatk/CMakePresets.json | 57 + libs/bsatk/README.md | 3 + libs/bsatk/Version.cmake | 3 + libs/bsatk/cmake/config.cmake.in | 10 + libs/bsatk/include/bsatk/bsaarchive.h | 244 + libs/bsatk/include/bsatk/bsaexception.h | 51 + libs/bsatk/include/bsatk/bsafile.h | 145 + libs/bsatk/include/bsatk/bsafolder.h | 170 + libs/bsatk/include/bsatk/bsatk.h | 15 + libs/bsatk/include/bsatk/bsatypes.h | 133 + libs/bsatk/include/bsatk/errorcodes.h | 41 + libs/bsatk/include/bsatk/filehash.h | 30 + libs/bsatk/src/CMakeLists.txt | 83 + libs/bsatk/src/bsaarchive.cpp | 1237 +++ libs/bsatk/src/bsaexception.cpp | 39 + libs/bsatk/src/bsafile.cpp | 154 + libs/bsatk/src/bsafolder.cpp | 342 + libs/bsatk/src/bsatypes.cpp | 70 + libs/bsatk/src/filehash.cpp | 100 + libs/bsatk/vcpkg.json | 30 + libs/check_fnis/.clang-format | 41 + libs/check_fnis/.gitattributes | 7 + libs/check_fnis/.gitignore | 5 + libs/check_fnis/.pre-commit-config.yaml | 20 + libs/check_fnis/CMakeLists.txt | 5 + libs/check_fnis/CMakePresets.json | 57 + libs/check_fnis/src/CMakeLists.txt | 14 + libs/check_fnis/src/check_fnis_en.ts | 66 + libs/check_fnis/src/checkfnis.cpp | 293 + libs/check_fnis/src/checkfnis.h | 67 + libs/check_fnis/vcpkg.json | 15 + libs/cmake_common/.pre-commit-config.yaml | 15 + libs/cmake_common/README.md | 109 + libs/cmake_common/mo2-cmake-config.cmake | 3 + libs/cmake_common/mo2.cmake | 48 + libs/cmake_common/mo2_cpp.cmake | 358 + libs/cmake_common/mo2_python.cmake | 356 + libs/cmake_common/mo2_utils.cmake | 464 + libs/cmake_common/mo2_versions.cmake | 49 + libs/dds-header/include/DDS/DDS.h | 331 + libs/dds-header/include/DDS/dxgiformat.h | 95 + libs/dds-header/mo2-dds-header-config.cmake | 6 + libs/dds_header/.editorconfig | 9 + libs/dds_header/.gitignore | 109 + libs/dds_header/.pre-commit-config.yaml | 15 + libs/dds_header/CMakeLists.txt | 5 + libs/dds_header/CMakePresets.json | 11 + libs/dds_header/LICENSE | 674 ++ libs/dds_header/README.md | 2 + libs/dds_header/src/CMakeLists.txt | 6 + libs/dds_header/src/DDS/DDSDefinitions.py | 808 ++ libs/dds_header/src/DDS/DDSFile.py | 220 + libs/dds_header/src/DDS/__init__.py | 0 libs/dds_header/src/DDS/glstuff.py | 205 + libs/dds_header/src/DDSPreview.py | 528 + libs/dds_header/src/DDSPreview_en.ts | 128 + libs/dds_header/vcpkg.json | 15 + libs/diagnose_basic/.clang-format | 41 + libs/diagnose_basic/.gitattributes | 7 + libs/diagnose_basic/.github/workflows/build.yml | 20 + libs/diagnose_basic/.github/workflows/linting.yml | 16 + libs/diagnose_basic/.gitignore | 6 + libs/diagnose_basic/.pre-commit-config.yaml | 20 + libs/diagnose_basic/CMakeLists.txt | 5 + libs/diagnose_basic/CMakePresets.json | 52 + libs/diagnose_basic/src/CMakeLists.txt | 13 + libs/diagnose_basic/src/diagnose_basic_en.ts | 178 + libs/diagnose_basic/src/diagnosebasic.cpp | 803 ++ libs/diagnose_basic/src/diagnosebasic.h | 142 + libs/diagnose_basic/vcpkg.json | 23 + libs/esptk/.clang-format | 41 + libs/esptk/.gitattributes | 7 + libs/esptk/.github/workflows/build.yml | 30 + libs/esptk/.github/workflows/linting.yml | 16 + libs/esptk/.gitignore | 5 + libs/esptk/.pre-commit-config.yaml | 20 + libs/esptk/CMakeLists.txt | 30 + libs/esptk/CMakePresets.json | 36 + libs/esptk/README.md | 3 + libs/esptk/Version.cmake | 3 + libs/esptk/cmake/config.cmake.in | 3 + libs/esptk/include/esptk/espexceptions.h | 23 + libs/esptk/include/esptk/espfile.h | 72 + libs/esptk/include/esptk/esptypes.h | 20 + libs/esptk/include/esptk/record.h | 60 + libs/esptk/include/esptk/subrecord.h | 44 + libs/esptk/include/esptk/tes3record.h | 32 + libs/esptk/include/esptk/tes3subrecord.h | 42 + libs/esptk/src/CMakeLists.txt | 55 + libs/esptk/src/espfile.cpp | 188 + libs/esptk/src/record.cpp | 40 + libs/esptk/src/subrecord.cpp | 59 + libs/esptk/src/tes3record.cpp | 16 + libs/esptk/src/tes3subrecord.cpp | 52 + libs/fnistool/.editorconfig | 9 + libs/fnistool/.gitignore | 109 + libs/fnistool/.pre-commit-config.yaml | 15 + libs/fnistool/CMakeLists.txt | 5 + libs/fnistool/CMakePresets.json | 17 + libs/fnistool/LICENSE | 674 ++ libs/fnistool/src/CMakeLists.txt | 6 + libs/fnistool/src/FNISPatches.py | 245 + libs/fnistool/src/FNISTool.py | 342 + libs/fnistool/src/FNISToolReset.py | 76 + libs/fnistool/src/FNISTool_en.ts | 295 + libs/fnistool/vcpkg.json | 15 + libs/form43_checker/.editorconfig | 5 + libs/form43_checker/.gitignore | 109 + libs/form43_checker/.pre-commit-config.yaml | 15 + libs/form43_checker/CMakeLists.txt | 4 + libs/form43_checker/CMakePresets.json | 17 + libs/form43_checker/LICENSE | 674 ++ libs/form43_checker/src/CMakeLists.txt | 6 + libs/form43_checker/src/Form43Checker.py | 105 + libs/form43_checker/src/Form43Checker_en.ts | 27 + libs/form43_checker/vcpkg.json | 15 + libs/game_bethesda/.clang-format | 41 + libs/game_bethesda/.git-blame-ignore-revs | 15 + libs/game_bethesda/.gitattributes | 7 + libs/game_bethesda/.github/workflows/build.yml | 20 + libs/game_bethesda/.github/workflows/linting.yml | 16 + libs/game_bethesda/.gitignore | 5 + libs/game_bethesda/.pre-commit-config.yaml | 20 + libs/game_bethesda/CMakeLists.txt | 56 + libs/game_bethesda/CMakePresets.json | 57 + libs/game_bethesda/src/creation/CMakeLists.txt | 33 + .../src/creation/creationgameplugins.cpp | 183 + .../src/creation/creationgameplugins.h | 22 + .../game_bethesda/src/creation/game_creation_en.ts | 12 + libs/game_bethesda/src/gamebryo/CMakeLists.txt | 78 + libs/game_bethesda/src/gamebryo/dummybsa.cpp | 198 + libs/game_bethesda/src/gamebryo/dummybsa.h | 59 + .../game_bethesda/src/gamebryo/game_gamebryo_en.ts | 171 + .../src/gamebryo/gamebryobsainvalidation.cpp | 165 + .../src/gamebryo/gamebryobsainvalidation.h | 36 + .../src/gamebryo/gamebryodataarchives.cpp | 68 + .../src/gamebryo/gamebryodataarchives.h | 37 + .../src/gamebryo/gamebryogameplugins.cpp | 246 + .../src/gamebryo/gamebryogameplugins.h | 38 + .../src/gamebryo/gamebryolocalsavegames.cpp | 138 + .../src/gamebryo/gamebryolocalsavegames.h | 54 + .../src/gamebryo/gamebryomoddatachecker.cpp | 76 + .../src/gamebryo/gamebryomoddatachecker.h | 46 + .../src/gamebryo/gamebryomoddatacontent.cpp | 116 + .../src/gamebryo/gamebryomoddatacontent.h | 75 + .../src/gamebryo/gamebryosavegame.cpp | 607 ++ libs/game_bethesda/src/gamebryo/gamebryosavegame.h | 256 + .../src/gamebryo/gamebryosavegameinfo.cpp | 100 + .../src/gamebryo/gamebryosavegameinfo.h | 23 + .../src/gamebryo/gamebryosavegameinfowidget.cpp | 199 + .../src/gamebryo/gamebryosavegameinfowidget.h | 30 + .../src/gamebryo/gamebryosavegameinfowidget.ui | 209 + .../src/gamebryo/gamebryoscriptextender.cpp | 46 + .../src/gamebryo/gamebryoscriptextender.h | 31 + .../src/gamebryo/gamebryounmanagedmods.cpp | 51 + .../src/gamebryo/gamebryounmanagedmods.h | 26 + libs/game_bethesda/src/gamebryo/gamegamebryo.cpp | 823 ++ libs/game_bethesda/src/gamebryo/gamegamebryo.h | 169 + libs/game_bethesda/src/gamebryo/vdf_parser.h | 728 ++ .../game_bethesda/src/games/enderal/CMakeLists.txt | 24 + .../src/games/enderal/enderalbsainvalidation.cpp | 16 + .../src/games/enderal/enderalbsainvalidation.h | 20 + .../src/games/enderal/enderaldataarchives.cpp | 42 + .../src/games/enderal/enderaldataarchives.h | 23 + .../src/games/enderal/enderalgameplugins.cpp | 123 + .../src/games/enderal/enderalgameplugins.h | 23 + .../src/games/enderal/enderallocalsavegames.cpp | 24 + .../src/games/enderal/enderallocalsavegames.h | 35 + .../src/games/enderal/enderalmoddatachecker.h | 49 + .../src/games/enderal/enderalmoddatacontent.h | 14 + .../src/games/enderal/enderalsavegame.cpp | 101 + .../src/games/enderal/enderalsavegame.h | 23 + .../src/games/enderal/enderalscriptextender.cpp | 18 + .../src/games/enderal/enderalscriptextender.h | 17 + .../src/games/enderal/game_enderal_en.ts | 17 + .../src/games/enderal/gameenderal.cpp | 267 + libs/game_bethesda/src/games/enderal/gameenderal.h | 61 + .../src/games/enderal/gameenderal.json | 1 + .../src/games/enderalse/CMakeLists.txt | 24 + .../src/games/enderalse/enderalsedataarchives.cpp | 64 + .../src/games/enderalse/enderalsedataarchives.h | 26 + .../src/games/enderalse/enderalsegameplugins.cpp | 82 + .../src/games/enderalse/enderalsegameplugins.h | 19 + .../games/enderalse/enderalselocalsavegames.cpp | 24 + .../src/games/enderalse/enderalselocalsavegames.h | 17 + .../src/games/enderalse/enderalsemoddatachecker.h | 31 + .../src/games/enderalse/enderalsemoddatacontent.h | 21 + .../src/games/enderalse/enderalsesavegame.cpp | 148 + .../src/games/enderalse/enderalsesavegame.h | 28 + .../games/enderalse/enderalsescriptextender.cpp | 18 + .../src/games/enderalse/enderalsescriptextender.h | 17 + .../src/games/enderalse/enderalseunmanagedmods.cpp | 29 + .../src/games/enderalse/enderalseunmanagedmods.h | 16 + .../src/games/enderalse/game_enderalse_en.ts | 25 + .../src/games/enderalse/gameenderalse.cpp | 347 + .../src/games/enderalse/gameenderalse.h | 75 + .../src/games/enderalse/gameenderalse.json | 1 + .../src/games/fallout3/CMakeLists.txt | 20 + .../src/games/fallout3/fallout3bsainvalidation.cpp | 16 + .../src/games/fallout3/fallout3bsainvalidation.h | 20 + .../src/games/fallout3/fallout3dataarchives.cpp | 33 + .../src/games/fallout3/fallout3dataarchives.h | 20 + .../src/games/fallout3/fallout3moddatachecker.h | 31 + .../src/games/fallout3/fallout3moddatacontent.h | 22 + .../src/games/fallout3/fallout3savegame.cpp | 72 + .../src/games/fallout3/fallout3savegame.h | 23 + .../src/games/fallout3/fallout3scriptextender.cpp | 18 + .../src/games/fallout3/fallout3scriptextender.h | 17 + .../src/games/fallout3/game_fallout3_en.ts | 17 + .../src/games/fallout3/gamefallout3.cpp | 279 + .../src/games/fallout3/gamefallout3.h | 58 + .../src/games/fallout3/gamefallout3.json | 1 + .../src/games/fallout4/CMakeLists.txt | 22 + libs/game_bethesda/src/games/fallout4/fallout4.qrc | 5 + .../src/games/fallout4/fallout4bsainvalidation.cpp | 68 + .../src/games/fallout4/fallout4bsainvalidation.h | 33 + .../src/games/fallout4/fallout4dataarchives.cpp | 48 + .../src/games/fallout4/fallout4dataarchives.h | 27 + .../src/games/fallout4/fallout4moddatachecker.h | 29 + .../src/games/fallout4/fallout4moddatacontent.h | 44 + .../src/games/fallout4/fallout4savegame.cpp | 106 + .../src/games/fallout4/fallout4savegame.h | 23 + .../src/games/fallout4/fallout4scriptextender.cpp | 18 + .../src/games/fallout4/fallout4scriptextender.h | 17 + .../src/games/fallout4/fallout4unmanagedmods.cpp | 63 + .../src/games/fallout4/fallout4unmanagedmods.h | 18 + .../src/games/fallout4/game_fallout4_en.ts | 28 + .../src/games/fallout4/gamefallout4.cpp | 334 + .../src/games/fallout4/gamefallout4.h | 73 + .../src/games/fallout4/gamefallout4.json | 1 + libs/game_bethesda/src/games/fallout4/splash.png | Bin 0 -> 52981 bytes .../src/games/fallout4london/CMakeLists.txt | 20 + .../src/games/fallout4london/fo4london.qrc | 5 + .../fallout4london/fo4londonbsainvalidation.cpp | 68 + .../fallout4london/fo4londonbsainvalidation.h | 33 + .../games/fallout4london/fo4londondataarchives.cpp | 48 + .../games/fallout4london/fo4londondataarchives.h | 27 + .../games/fallout4london/fo4londonmoddatachecker.h | 29 + .../games/fallout4london/fo4londonmoddatacontent.h | 44 + .../src/games/fallout4london/fo4londonsavegame.cpp | 108 + .../src/games/fallout4london/fo4londonsavegame.h | 23 + .../fallout4london/fo4londonscriptextender.cpp | 18 + .../games/fallout4london/fo4londonscriptextender.h | 17 + .../fallout4london/fo4londonunmanagedmods.cpp | 63 + .../games/fallout4london/fo4londonunmanagedmods.h | 18 + .../src/games/fallout4london/game_fo4london_en.ts | 27 + .../src/games/fallout4london/gamefo4london.cpp | 366 + .../src/games/fallout4london/gamefo4london.h | 78 + .../src/games/fallout4london/gamefo4london.json | 1 + .../src/games/fallout4london/splash.png | Bin 0 -> 52981 bytes .../src/games/fallout4vr/CMakeLists.txt | 22 + .../src/games/fallout4vr/fallout4vr.qrc | 5 + .../games/fallout4vr/fallout4vrdataarchives.cpp | 49 + .../src/games/fallout4vr/fallout4vrdataarchives.h | 27 + .../src/games/fallout4vr/fallout4vrgameplugins.cpp | 15 + .../src/games/fallout4vr/fallout4vrgameplugins.h | 19 + .../games/fallout4vr/fallout4vrmoddatachecker.h | 30 + .../games/fallout4vr/fallout4vrmoddatacontent.h | 44 + .../src/games/fallout4vr/fallout4vrsavegame.cpp | 108 + .../src/games/fallout4vr/fallout4vrsavegame.h | 23 + .../games/fallout4vr/fallout4vrscriptextender.cpp | 18 + .../games/fallout4vr/fallout4vrscriptextender.h | 17 + .../games/fallout4vr/fallout4vrunmanagedmods.cpp | 63 + .../src/games/fallout4vr/fallout4vrunmanagedmods.h | 18 + .../src/games/fallout4vr/game_fallout4vr_en.ts | 18 + .../src/games/fallout4vr/gamefallout4vr.cpp | 251 + .../src/games/fallout4vr/gamefallout4vr.h | 59 + .../src/games/fallout4vr/gamefallout4vr.json | 1 + libs/game_bethesda/src/games/fallout4vr/splash.png | Bin 0 -> 52981 bytes .../src/games/fallout76/CMakeLists.txt | 22 + .../src/games/fallout76/fallout76.qrc | 5 + .../src/games/fallout76/fallout76dataarchives.cpp | 150 + .../src/games/fallout76/fallout76dataarchives.h | 33 + .../src/games/fallout76/fallout76moddatachecker.h | 44 + .../src/games/fallout76/fallout76moddatacontent.h | 44 + .../src/games/fallout76/fallout76savegame.cpp | 106 + .../src/games/fallout76/fallout76savegame.h | 23 + .../src/games/fallout76/fallout76savegameinfo.cpp | 10 + .../src/games/fallout76/fallout76savegameinfo.h | 15 + .../games/fallout76/fallout76scriptextender.cpp | 18 + .../src/games/fallout76/fallout76scriptextender.h | 17 + .../src/games/fallout76/fallout76unmanagedmods.cpp | 46 + .../src/games/fallout76/fallout76unmanagedmods.h | 18 + .../src/games/fallout76/game_fallout76_en.ts | 13 + .../src/games/fallout76/gamefallout76.cpp | 227 + .../src/games/fallout76/gamefallout76.h | 55 + .../src/games/fallout76/gamefallout76.json | 1 + libs/game_bethesda/src/games/fallout76/splash.png | Bin 0 -> 52981 bytes .../src/games/falloutnv/CMakeLists.txt | 31 + .../games/falloutnv/falloutnvbsainvalidation.cpp | 16 + .../src/games/falloutnv/falloutnvbsainvalidation.h | 20 + .../src/games/falloutnv/falloutnvdataarchives.cpp | 32 + .../src/games/falloutnv/falloutnvdataarchives.h | 23 + .../src/games/falloutnv/falloutnvmoddatachecker.h | 51 + .../src/games/falloutnv/falloutnvmoddatacontent.h | 22 + .../src/games/falloutnv/falloutnvsavegame.cpp | 77 + .../src/games/falloutnv/falloutnvsavegame.h | 23 + .../games/falloutnv/falloutnvscriptextender.cpp | 18 + .../src/games/falloutnv/falloutnvscriptextender.h | 17 + .../src/games/falloutnv/game_falloutNV_en.ts | 22 + .../src/games/falloutnv/gamefalloutnv.cpp | 334 + .../src/games/falloutnv/gamefalloutnv.h | 70 + .../src/games/falloutnv/gamefalloutnv.json | 1 + .../src/games/morrowind/CMakeLists.txt | 27 + .../src/games/morrowind/game_morrowind_en.ts | 69 + .../src/games/morrowind/gamemorrowind.cpp | 216 + .../src/games/morrowind/gamemorrowind.h | 62 + .../src/games/morrowind/gamemorrowind.json | 1 + .../src/games/morrowind/morrowind.qrc | 5 + .../games/morrowind/morrowindbsainvalidation.cpp | 16 + .../src/games/morrowind/morrowindbsainvalidation.h | 20 + .../src/games/morrowind/morrowinddataarchives.cpp | 71 + .../src/games/morrowind/morrowinddataarchives.h | 28 + .../src/games/morrowind/morrowindgameplugins.cpp | 197 + .../src/games/morrowind/morrowindgameplugins.h | 28 + .../games/morrowind/morrowindlocalsavegames.cpp | 56 + .../src/games/morrowind/morrowindlocalsavegames.h | 41 + .../src/games/morrowind/morrowindmoddatachecker.h | 26 + .../src/games/morrowind/morrowindmoddatacontent.h | 24 + .../src/games/morrowind/morrowindsavegame.cpp | 183 + .../src/games/morrowind/morrowindsavegame.h | 57 + .../src/games/morrowind/morrowindsavegameinfo.cpp | 18 + .../src/games/morrowind/morrowindsavegameinfo.h | 22 + .../morrowind/morrowindsavegameinfowidget.cpp | 123 + .../games/morrowind/morrowindsavegameinfowidget.h | 31 + .../games/morrowind/morrowindsavegameinfowidget.ui | 248 + libs/game_bethesda/src/games/morrowind/splash.png | Bin 0 -> 83336 bytes libs/game_bethesda/src/games/nehrim/CMakeLists.txt | 21 + .../src/games/nehrim/game_nehrim_en.ts | 17 + libs/game_bethesda/src/games/nehrim/gamenehrim.cpp | 205 + libs/game_bethesda/src/games/nehrim/gamenehrim.h | 56 + .../game_bethesda/src/games/nehrim/gamenehrim.json | 1 + .../src/games/nehrim/nehrimbsainvalidation.cpp | 16 + .../src/games/nehrim/nehrimbsainvalidation.h | 20 + .../src/games/nehrim/nehrimdataarchives.cpp | 31 + .../src/games/nehrim/nehrimdataarchives.h | 23 + .../src/games/nehrim/nehrimmoddatachecker.cpp | 30 + .../src/games/nehrim/nehrimmoddatachecker.h | 35 + .../src/games/nehrim/nehrimmoddatacontent.h | 22 + .../src/games/nehrim/nehrimsavegame.cpp | 72 + .../src/games/nehrim/nehrimsavegame.h | 21 + .../src/games/nehrim/nehrimscriptextender.cpp | 20 + .../src/games/nehrim/nehrimscriptextender.h | 18 + .../src/games/oblivion/CMakeLists.txt | 21 + .../src/games/oblivion/game_oblivion_en.ts | 17 + .../src/games/oblivion/gameoblivion.cpp | 213 + .../src/games/oblivion/gameoblivion.h | 51 + .../src/games/oblivion/gameoblivion.json | 1 + .../src/games/oblivion/oblivionbsainvalidation.cpp | 16 + .../src/games/oblivion/oblivionbsainvalidation.h | 20 + .../src/games/oblivion/obliviondataarchives.cpp | 32 + .../src/games/oblivion/obliviondataarchives.h | 23 + .../src/games/oblivion/oblivionmoddatachecker.cpp | 30 + .../src/games/oblivion/oblivionmoddatachecker.h | 35 + .../src/games/oblivion/oblivionmoddatacontent.h | 22 + .../src/games/oblivion/oblivionsavegame.cpp | 74 + .../src/games/oblivion/oblivionsavegame.h | 21 + .../src/games/oblivion/oblivionscriptextender.cpp | 20 + .../src/games/oblivion/oblivionscriptextender.h | 18 + libs/game_bethesda/src/games/skyrim/CMakeLists.txt | 22 + .../src/games/skyrim/game_skyrim_en.ts | 17 + libs/game_bethesda/src/games/skyrim/gameskyrim.cpp | 263 + libs/game_bethesda/src/games/skyrim/gameskyrim.h | 55 + .../game_bethesda/src/games/skyrim/gameskyrim.json | 1 + .../src/games/skyrim/skyrimbsainvalidation.cpp | 16 + .../src/games/skyrim/skyrimbsainvalidation.h | 20 + .../src/games/skyrim/skyrimdataarchives.cpp | 42 + .../src/games/skyrim/skyrimdataarchives.h | 23 + .../src/games/skyrim/skyrimgameplugins.cpp | 123 + .../src/games/skyrim/skyrimgameplugins.h | 23 + .../src/games/skyrim/skyrimmoddatachecker.h | 50 + .../src/games/skyrim/skyrimmoddatacontent.h | 14 + .../src/games/skyrim/skyrimsavegame.cpp | 105 + .../src/games/skyrim/skyrimsavegame.h | 28 + .../src/games/skyrim/skyrimscriptextender.cpp | 18 + .../src/games/skyrim/skyrimscriptextender.h | 17 + .../src/games/skyrimse/CMakeLists.txt | 36 + .../src/games/skyrimse/game_skyrimse_en.ts | 17 + .../src/games/skyrimse/gameskyrimse.cpp | 359 + .../src/games/skyrimse/gameskyrimse.h | 72 + .../src/games/skyrimse/skyrimsedataarchives.cpp | 44 + .../src/games/skyrimse/skyrimsedataarchives.h | 25 + .../src/games/skyrimse/skyrimsemoddatachecker.h | 42 + .../src/games/skyrimse/skyrimsemoddatacontent.h | 21 + .../src/games/skyrimse/skyrimsesavegame.cpp | 157 + .../src/games/skyrimse/skyrimsesavegame.h | 28 + .../src/games/skyrimse/skyrimsescriptextender.cpp | 18 + .../src/games/skyrimse/skyrimsescriptextender.h | 17 + .../src/games/skyrimse/skyrimseunmanagedmods.cpp | 29 + .../src/games/skyrimse/skyrimseunmanagedmods.h | 16 + .../src/games/skyrimvr/CMakeLists.txt | 22 + .../src/games/skyrimvr/game_skyrimvr_en.ts | 17 + .../src/games/skyrimvr/gameskyrimvr.cpp | 306 + .../src/games/skyrimvr/gameskyrimvr.h | 70 + .../src/games/skyrimvr/gameskyrimvr.json | 1 + .../src/games/skyrimvr/skyrimvrdataarchives.cpp | 45 + .../src/games/skyrimvr/skyrimvrdataarchives.h | 26 + .../src/games/skyrimvr/skyrimvrgameplugins.cpp | 15 + .../src/games/skyrimvr/skyrimvrgameplugins.h | 19 + .../src/games/skyrimvr/skyrimvrmoddatachecker.h | 31 + .../src/games/skyrimvr/skyrimvrmoddatacontent.h | 21 + .../src/games/skyrimvr/skyrimvrsavegame.cpp | 149 + .../src/games/skyrimvr/skyrimvrsavegame.h | 24 + .../src/games/skyrimvr/skyrimvrscriptextender.cpp | 18 + .../src/games/skyrimvr/skyrimvrscriptextender.h | 17 + .../src/games/skyrimvr/skyrimvrunmanagedmods.cpp | 29 + .../src/games/skyrimvr/skyrimvrunmanagedmods.h | 16 + .../src/games/starfield/CMakeLists.txt | 24 + .../src/games/starfield/game_starfield_en.ts | 73 + .../src/games/starfield/gamestarfield.cpp | 438 + .../src/games/starfield/gamestarfield.h | 83 + .../src/games/starfield/gamestarfield.json | 1 + .../games/starfield/starfieldbsainvalidation.cpp | 68 + .../src/games/starfield/starfieldbsainvalidation.h | 33 + .../src/games/starfield/starfielddataarchives.cpp | 98 + .../src/games/starfield/starfielddataarchives.h | 28 + .../src/games/starfield/starfieldgameplugins.cpp | 35 + .../src/games/starfield/starfieldgameplugins.h | 23 + .../src/games/starfield/starfieldmoddatachecker.h | 29 + .../src/games/starfield/starfieldmoddatacontent.h | 55 + .../src/games/starfield/starfieldsavegame.cpp | 157 + .../src/games/starfield/starfieldsavegame.h | 27 + .../games/starfield/starfieldscriptextender.cpp | 18 + .../src/games/starfield/starfieldscriptextender.h | 17 + .../src/games/starfield/starfieldunmanagedmods.cpp | 132 + .../src/games/starfield/starfieldunmanagedmods.h | 36 + libs/game_bethesda/src/games/ttw/CMakeLists.txt | 20 + .../src/games/ttw/falloutttwbsainvalidation.cpp | 16 + .../src/games/ttw/falloutttwbsainvalidation.h | 20 + .../src/games/ttw/falloutttwdataarchives.cpp | 31 + .../src/games/ttw/falloutttwdataarchives.h | 23 + .../src/games/ttw/falloutttwmoddatachecker.h | 51 + .../src/games/ttw/falloutttwmoddatacontent.h | 22 + .../src/games/ttw/falloutttwsavegame.cpp | 79 + .../src/games/ttw/falloutttwsavegame.h | 23 + .../src/games/ttw/falloutttwscriptextender.cpp | 18 + .../src/games/ttw/falloutttwscriptextender.h | 17 + libs/game_bethesda/src/games/ttw/game_ttw_en.ts | 22 + .../game_bethesda/src/games/ttw/gamefalloutttw.cpp | 364 + libs/game_bethesda/src/games/ttw/gamefalloutttw.h | 77 + .../src/games/ttw/gamefalloutttw.json | 1 + libs/game_bethesda/vcpkg.json | 31 + libs/game_features/.gitignore | 4 + libs/game_features/CMakeLists.txt | 10 + libs/game_features/src/CMakeLists.txt | 6 + libs/game_features/src/bsainvalidation.h | 24 + libs/game_features/src/dataarchives.h | 32 + libs/game_features/src/gamefeatures.pro | 25 + libs/game_features/src/gamefeatures_en.ts | 4 + libs/game_features/src/gameplugins.h | 22 + libs/game_features/src/localsavegames.h | 24 + libs/game_features/src/moddatachecker.h | 68 + libs/game_features/src/moddatacontent.h | 112 + libs/game_features/src/savegameinfo.h | 49 + libs/game_features/src/scriptextender.h | 41 + libs/game_features/src/unmanagedmods.h | 44 + libs/game_gamebryo/.clang-format | 41 + libs/game_gamebryo/.git-blame-ignore-revs | 1 + libs/game_gamebryo/.gitattributes | 7 + libs/game_gamebryo/.github/workflows/build.yml | 17 + libs/game_gamebryo/.github/workflows/linting.yml | 16 + libs/game_gamebryo/.gitignore | 5 + libs/game_gamebryo/CMakeLists.txt | 12 + libs/game_gamebryo/README.md | 5 + libs/game_gamebryo/src/creation/CMakeLists.txt | 10 + .../src/creation/creationgameplugins.cpp | 183 + .../src/creation/creationgameplugins.h | 22 + .../game_gamebryo/src/creation/game_creation_en.ts | 12 + libs/game_gamebryo/src/gamebryo/CMakeLists.txt | 10 + libs/game_gamebryo/src/gamebryo/dummybsa.cpp | 192 + libs/game_gamebryo/src/gamebryo/dummybsa.h | 59 + .../game_gamebryo/src/gamebryo/game_gamebryo_en.ts | 170 + .../src/gamebryo/gamebryobsainvalidation.cpp | 133 + .../src/gamebryo/gamebryobsainvalidation.h | 36 + .../src/gamebryo/gamebryodataarchives.cpp | 80 + .../src/gamebryo/gamebryodataarchives.h | 37 + .../src/gamebryo/gamebryogameplugins.cpp | 248 + .../src/gamebryo/gamebryogameplugins.h | 38 + .../src/gamebryo/gamebryolocalsavegames.cpp | 140 + .../src/gamebryo/gamebryolocalsavegames.h | 54 + .../src/gamebryo/gamebryomoddatachecker.cpp | 76 + .../src/gamebryo/gamebryomoddatachecker.h | 46 + .../src/gamebryo/gamebryomoddatacontent.cpp | 116 + .../src/gamebryo/gamebryomoddatacontent.h | 75 + .../src/gamebryo/gamebryosavegame.cpp | 606 ++ libs/game_gamebryo/src/gamebryo/gamebryosavegame.h | 240 + .../src/gamebryo/gamebryosavegameinfo.cpp | 100 + .../src/gamebryo/gamebryosavegameinfo.h | 23 + .../src/gamebryo/gamebryosavegameinfowidget.cpp | 199 + .../src/gamebryo/gamebryosavegameinfowidget.h | 30 + .../src/gamebryo/gamebryosavegameinfowidget.ui | 209 + .../src/gamebryo/gamebryoscriptextender.cpp | 46 + .../src/gamebryo/gamebryoscriptextender.h | 31 + .../src/gamebryo/gamebryounmanagedmods.cpp | 51 + .../src/gamebryo/gamebryounmanagedmods.h | 26 + libs/game_gamebryo/src/gamebryo/gamegamebryo.cpp | 478 + libs/game_gamebryo/src/gamebryo/gamegamebryo.h | 151 + libs/game_gamebryo/src/gamebryo/vdf_parser.h | 739 ++ libs/installer_bain/.clang-format | 41 + libs/installer_bain/.git-blame-ignore-revs | 1 + libs/installer_bain/.gitattributes | 7 + libs/installer_bain/.github/workflows/build.yml | 20 + libs/installer_bain/.github/workflows/linting.yml | 16 + libs/installer_bain/.gitignore | 5 + libs/installer_bain/.pre-commit-config.yaml | 20 + libs/installer_bain/CMakeLists.txt | 5 + libs/installer_bain/CMakePresets.json | 67 + libs/installer_bain/src/CMakeLists.txt | 13 + .../src/baincomplexinstallerdialog.cpp | 121 + .../src/baincomplexinstallerdialog.h | 94 + .../src/baincomplexinstallerdialog.ui | 126 + libs/installer_bain/src/installer_bain_en.ts | 83 + libs/installer_bain/src/installerbain.cpp | 222 + libs/installer_bain/src/installerbain.h | 80 + libs/installer_bain/vcpkg.json | 15 + libs/installer_bundle/.clang-format | 41 + libs/installer_bundle/.git-blame-ignore-revs | 1 + libs/installer_bundle/.gitattributes | 7 + libs/installer_bundle/.github/workflows/build.yml | 20 + .../installer_bundle/.github/workflows/linting.yml | 16 + libs/installer_bundle/.gitignore | 5 + libs/installer_bundle/.pre-commit-config.yaml | 20 + libs/installer_bundle/CMakeLists.txt | 5 + libs/installer_bundle/CMakePresets.json | 67 + libs/installer_bundle/src/CMakeLists.txt | 14 + libs/installer_bundle/src/installer_bundle_en.ts | 46 + libs/installer_bundle/src/installerbundle.cpp | 194 + libs/installer_bundle/src/installerbundle.h | 88 + libs/installer_bundle/src/multiarchivedialog.cpp | 40 + libs/installer_bundle/src/multiarchivedialog.h | 78 + libs/installer_bundle/src/multiarchivedialog.ui | 77 + libs/installer_bundle/vcpkg.json | 15 + libs/installer_fomod/.clang-format | 41 + libs/installer_fomod/.git-blame-ignore-revs | 1 + libs/installer_fomod/.gitattributes | 7 + libs/installer_fomod/.github/workflows/build.yml | 20 + libs/installer_fomod/.github/workflows/linting.yml | 16 + libs/installer_fomod/.gitignore | 5 + libs/installer_fomod/.pre-commit-config.yaml | 20 + libs/installer_fomod/CMakeLists.txt | 36 + libs/installer_fomod/CMakePresets.json | 67 + libs/installer_fomod/src/CMakeLists.txt | 18 + libs/installer_fomod/src/fomodinstallerdialog.cpp | 1701 ++++ libs/installer_fomod/src/fomodinstallerdialog.h | 476 + libs/installer_fomod/src/fomodinstallerdialog.ui | 269 + libs/installer_fomod/src/fomodscreenshotdialog.cpp | 170 + libs/installer_fomod/src/fomodscreenshotdialog.h | 35 + libs/installer_fomod/src/fomodscreenshotdialog.ui | 475 + libs/installer_fomod/src/installer_fomod_en.ts | 274 + libs/installer_fomod/src/installerfomod.cpp | 301 + libs/installer_fomod/src/installerfomod.h | 100 + libs/installer_fomod/src/resources.qrc | 7 + .../src/resources/CloseButtonIcon.png | Bin 0 -> 462 bytes .../src/resources/LeftButtonIcon.png | Bin 0 -> 380 bytes .../src/resources/RightButtonIcon.png | Bin 0 -> 386 bytes libs/installer_fomod/src/scalelabel.cpp | 107 + libs/installer_fomod/src/scalelabel.h | 31 + libs/installer_fomod/src/xmlreader.cpp | 83 + libs/installer_fomod/src/xmlreader.h | 40 + libs/installer_fomod/vcpkg.json | 15 + libs/installer_fomod_csharp/.clang-format | 41 + libs/installer_fomod_csharp/.git-blame-ignore-revs | 1 + libs/installer_fomod_csharp/.gitattributes | 7 + .../.github/workflows/build.yml | 20 + .../.github/workflows/linting.yml | 16 + libs/installer_fomod_csharp/.gitignore | 1 + .../installer_fomod_csharp/.pre-commit-config.yaml | 20 + libs/installer_fomod_csharp/CMakeLists.txt | 5 + libs/installer_fomod_csharp/CMakePresets.json | 67 + libs/installer_fomod_csharp/LICENSE | 674 ++ libs/installer_fomod_csharp/README.md | 20 + libs/installer_fomod_csharp/src/CMakeLists.txt | 11 + libs/installer_fomod_csharp/src/base_script.cpp | 685 ++ libs/installer_fomod_csharp/src/base_script.h | 552 + .../src/csharp_interface.cpp | 157 + libs/installer_fomod_csharp/src/csharp_interface.h | 40 + libs/installer_fomod_csharp/src/csharp_utils.h | 41 + .../src/installer_fomod_csharp.cpp | 193 + .../src/installer_fomod_csharp.h | 84 + .../src/installer_fomod_csharp_en.ts | 125 + .../src/installer_fomod_csharp_postdialog.ui | 99 + .../src/installer_fomod_csharp_predialog.ui | 89 + .../src/installer_fomod_postdialog.h | 87 + .../src/installer_fomod_predialog.h | 72 + libs/installer_fomod_csharp/src/psettings.h | 108 + libs/installer_fomod_csharp/src/xml_info_reader.h | 140 + libs/installer_fomod_csharp/vcpkg.json | 15 + libs/installer_manual/.clang-format | 41 + libs/installer_manual/.git-blame-ignore-revs | 1 + libs/installer_manual/.gitattributes | 7 + libs/installer_manual/.github/workflows/build.yml | 20 + .../installer_manual/.github/workflows/linting.yml | 16 + libs/installer_manual/.gitignore | 5 + libs/installer_manual/.pre-commit-config.yaml | 20 + libs/installer_manual/CMakeLists.txt | 5 + libs/installer_manual/CMakePresets.json | 67 + libs/installer_manual/src/CMakeLists.txt | 17 + libs/installer_manual/src/archivetree.cpp | 486 + libs/installer_manual/src/archivetree.h | 178 + libs/installer_manual/src/installdialog.cpp | 215 + libs/installer_manual/src/installdialog.h | 124 + libs/installer_manual/src/installdialog.ui | 162 + libs/installer_manual/src/installer_manual_en.ts | 164 + libs/installer_manual/src/installermanual.cpp | 136 + libs/installer_manual/src/installermanual.h | 72 + libs/installer_manual/vcpkg.json | 15 + libs/installer_omod/.editorconfig | 16 + libs/installer_omod/.gitattributes | 9 + libs/installer_omod/.github/workflows/build.yml | 20 + libs/installer_omod/.gitignore | 5 + libs/installer_omod/.pre-commit-config.yaml | 20 + libs/installer_omod/CMakeLists.txt | 9 + libs/installer_omod/CMakePresets.json | 59 + libs/installer_omod/LICENSE | 674 ++ libs/installer_omod/src/CMakeLists.txt | 67 + libs/installer_omod/src/DummyCSFile.cs | 16 + libs/installer_omod/src/MessageBoxHelper.cpp | 61 + libs/installer_omod/src/MessageBoxHelper.h | 37 + libs/installer_omod/src/OMODFrameworkWrapper.cpp | 771 ++ libs/installer_omod/src/OMODFrameworkWrapper.h | 69 + libs/installer_omod/src/QObject_unique_ptr.h | 20 + .../src/implementations/CodeProgress.cpp | 85 + .../src/implementations/CodeProgress.h | 59 + libs/installer_omod/src/implementations/Logger.cpp | 45 + libs/installer_omod/src/implementations/Logger.h | 17 + .../src/implementations/ScriptFunctions.cpp | 320 + .../src/implementations/ScriptFunctions.h | 122 + libs/installer_omod/src/installerOmod.cpp | 204 + libs/installer_omod/src/installerOmod.h | 75 + libs/installer_omod/src/installer_omod_en.ts | 159 + .../src/interop/QtDotNetConverters.cpp | 17 + .../src/interop/QtDotNetConverters.h | 9 + .../src/interop/StdDotNetConverters.cpp | 35 + .../src/interop/StdDotNetConverters.h | 13 + libs/installer_omod/src/newstuff/namedialog.cpp | 37 + libs/installer_omod/src/newstuff/namedialog.h | 28 + libs/installer_omod/src/newstuff/namedialog.ui | 89 + libs/installer_omod/src/newstuff/rtfPopup.cpp | 33 + libs/installer_omod/src/newstuff/rtfPopup.h | 11 + libs/installer_omod/src/oldstuff/DialogSelect.cpp | 301 + libs/installer_omod/src/oldstuff/DialogSelect.h | 46 + .../src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE | 21 + .../MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp | 165 + .../MIT-licencedCodeToDoStuff/checkboxwordwrap.h | 75 + .../MIT-licencedCodeToDoStuff/clickablelabel.cpp | 24 + .../MIT-licencedCodeToDoStuff/clickablelabel.h | 27 + libs/installer_omod/src/oldstuff/folder.md | 1 + libs/installer_omod/vcpkg.json | 15 + libs/installer_quick/.clang-format | 41 + libs/installer_quick/.git-blame-ignore-revs | 1 + libs/installer_quick/.gitattributes | 7 + libs/installer_quick/.github/workflows/build.yml | 20 + libs/installer_quick/.github/workflows/linting.yml | 16 + libs/installer_quick/.gitignore | 5 + libs/installer_quick/.pre-commit-config.yaml | 20 + libs/installer_quick/CMakeLists.txt | 4 + libs/installer_quick/CMakePresets.json | 67 + libs/installer_quick/src/CMakeLists.txt | 13 + libs/installer_quick/src/installer_quick_en.ts | 51 + libs/installer_quick/src/installerquick.cpp | 176 + libs/installer_quick/src/installerquick.h | 77 + libs/installer_quick/src/simpleinstalldialog.cpp | 69 + libs/installer_quick/src/simpleinstalldialog.h | 72 + libs/installer_quick/src/simpleinstalldialog.ui | 86 + libs/installer_quick/vcpkg.json | 15 + libs/installer_wizard/.gitattributes | 6 + libs/installer_wizard/.github/workflows/build.yml | 39 + .../installer_wizard/.github/workflows/linters.yml | 32 + libs/installer_wizard/.gitignore | 20 + libs/installer_wizard/.pre-commit-config.yaml | 25 + libs/installer_wizard/CMakeLists.txt | 7 + libs/installer_wizard/CMakePresets.json | 17 + libs/installer_wizard/LICENSE | 7 + libs/installer_wizard/README.md | 83 + libs/installer_wizard/make-release.ps1 | 29 + libs/installer_wizard/make.ps1 | 10 + libs/installer_wizard/plugin-requirements.txt | 1 + libs/installer_wizard/poetry.lock | 294 + libs/installer_wizard/pyproject.toml | 65 + libs/installer_wizard/src/CMakeLists.txt | 4 + libs/installer_wizard/src/__init__.py | 17 + libs/installer_wizard/src/dialog.py | 752 ++ libs/installer_wizard/src/installer.py | 397 + libs/installer_wizard/src/installer_wizard_en.ts | 156 + libs/installer_wizard/src/runner.py | 189 + .../src/ui/wizardinstallercomplete.ui | 271 + .../src/ui/wizardinstallerdialog.ui | 128 + .../src/ui/wizardinstallererror.ui | 103 + .../installer_wizard/src/ui/wizardinstallerpage.ui | 171 + .../src/ui/wizardinstallerrequires.ui | 220 + libs/installer_wizard/src/utils.py | 170 + libs/installer_wizard/vcpkg.json | 17 + libs/libbsarch/.clang-format | 117 + libs/libbsarch/.gitattributes | 7 + libs/libbsarch/.github/workflows/build.yml | 64 + libs/libbsarch/.github/workflows/linting.yml | 17 + libs/libbsarch/.gitignore | 539 + libs/libbsarch/.pre-commit-config.yaml | 20 + libs/libbsarch/CMakeLists.txt | 144 + libs/libbsarch/CMakePresets.json | 17 + libs/libbsarch/LICENSE | 21 + libs/libbsarch/README.md | 25 + libs/libbsarch/Version.cmake | 3 + libs/libbsarch/cmake/config.cmake.in | 37 + libs/libbsarch/delphi/libbsarch.dpr | 412 + libs/libbsarch/delphi/libbsarch.dproj | 916 ++ libs/libbsarch/delphi/wbBSArchive.pas | 2637 +++++ .../examples/libbsarch-visualstudio-test.cpp | 190 + .../examples/libbsarch-visualstudio-test.sln | 31 + libs/libbsarch/examples/test_read.ba2 | Bin 0 -> 5592601 bytes libs/libbsarch/examples/test_read.bsa | Bin 0 -> 5592685 bytes libs/libbsarch/include/libbsarch/base_types.hpp | 66 + libs/libbsarch/include/libbsarch/bs_archive.h | 100 + .../include/libbsarch/bs_archive_auto.hpp | 39 + .../include/libbsarch/bs_archive_entries.h | 24 + libs/libbsarch/include/libbsarch/libbsarch.h | 157 + libs/libbsarch/include/libbsarch/libbsarch.hpp | 37 + .../libbsarch/utils/convertible_ostream.hpp | 37 + .../include/libbsarch/utils/convertible_string.hpp | 50 + .../include/libbsarch/utils/string_convert.hpp | 18 + libs/libbsarch/src/bs_archive.cpp | 185 + libs/libbsarch/src/bs_archive_auto.cpp | 87 + libs/libbsarch/src/bs_archive_entries.cpp | 52 + libs/libbsarch/src/libbsarch.cpp | 206 + libs/libbsarch/src/libbsarch.def | 38 + libs/libbsarch/src/utils/convertible_ostream.cpp | 58 + libs/libbsarch/src/utils/convertible_string.cpp | 108 + libs/libbsarch/src/utils/string_convert.cpp | 39 + libs/libbsarch/vcpkg.json | 18 + libs/lootcli/.clang-format | 41 + libs/lootcli/.git-blame-ignore-revs | 2 + libs/lootcli/.gitattributes | 7 + libs/lootcli/.github/workflows/build.yml | 18 + libs/lootcli/.github/workflows/linting.yml | 15 + libs/lootcli/.gitignore | 5 + libs/lootcli/.pre-commit-config.yaml | 20 + libs/lootcli/CMakeLists.txt | 32 + libs/lootcli/CMakePresets.json | 52 + libs/lootcli/README.md | 3 + libs/lootcli/cmake/config.cmake.in | 3 + libs/lootcli/include/lootcli/lootcli.h | 116 + libs/lootcli/src/CMakeLists.txt | 138 + libs/lootcli/src/game_settings.cpp | 435 + libs/lootcli/src/game_settings.h | 101 + libs/lootcli/src/lootthread.cpp | 1197 +++ libs/lootcli/src/lootthread.h | 107 + libs/lootcli/src/main.cpp | 102 + libs/lootcli/src/pch.cpp | 1 + libs/lootcli/src/pch.h | 84 + libs/lootcli/src/version.h | 4 + libs/lootcli/src/version.rc | 35 + libs/lootcli/vcpkg.json | 30 + libs/lzokay.py | 6 + libs/nak_ffi/CMakeLists.txt | 33 + libs/nak_ffi/Cargo.lock | 2784 +++++ libs/nak_ffi/Cargo.toml | 10 + libs/nak_ffi/include/nak_ffi.h | 220 + libs/nak_ffi/src/lib.rs | 692 ++ libs/plugin_python/.clang-format | 21 + libs/plugin_python/.git-blame-ignore-revs | 2 + libs/plugin_python/.gitattributes | 8 + .../.github/workflows/build-and-test.yml | 38 + libs/plugin_python/.github/workflows/linting.yml | 24 + libs/plugin_python/.gitignore | 12 + libs/plugin_python/.pre-commit-config.yaml | 20 + libs/plugin_python/CMakeLists.txt | 51 + libs/plugin_python/CMakePresets.json | 73 + libs/plugin_python/README.md | 68 + libs/plugin_python/poetry.lock | 254 + libs/plugin_python/pyproject.toml | 53 + libs/plugin_python/src/CMakeLists.txt | 8 + libs/plugin_python/src/mobase/CMakeLists.txt | 40 + libs/plugin_python/src/mobase/README.md | 110 + libs/plugin_python/src/mobase/deprecation.cpp | 55 + libs/plugin_python/src/mobase/deprecation.h | 27 + libs/plugin_python/src/mobase/mobase.cpp | 86 + libs/plugin_python/src/mobase/pybind11_all.h | 146 + .../src/mobase/wrappers/basic_classes.cpp | 952 ++ .../src/mobase/wrappers/game_features.cpp | 422 + .../src/mobase/wrappers/known_folders.h | 155 + .../src/mobase/wrappers/pyfiletree.cpp | 329 + .../plugin_python/src/mobase/wrappers/pyfiletree.h | 34 + .../src/mobase/wrappers/pyplugins.cpp | 262 + libs/plugin_python/src/mobase/wrappers/pyplugins.h | 527 + libs/plugin_python/src/mobase/wrappers/utils.cpp | 50 + libs/plugin_python/src/mobase/wrappers/widgets.cpp | 77 + .../plugin_python/src/mobase/wrappers/wrappers.cpp | 120 + libs/plugin_python/src/mobase/wrappers/wrappers.h | 109 + libs/plugin_python/src/plugin_python_en.ts | 83 + libs/plugin_python/src/proxy/CMakeLists.txt | 118 + libs/plugin_python/src/proxy/build_pythoncore.py | 15 + libs/plugin_python/src/proxy/proxypython.cpp | 368 + libs/plugin_python/src/proxy/proxypython.h | 80 + libs/plugin_python/src/pybind11-qt/CMakeLists.txt | 60 + libs/plugin_python/src/pybind11-qt/README.md | 64 + .../include/pybind11_qt/details/pybind11_qt_enum.h | 59 + .../pybind11_qt/details/pybind11_qt_qlist.h | 52 + .../include/pybind11_qt/details/pybind11_qt_qmap.h | 70 + .../include/pybind11_qt/details/pybind11_qt_sip.h | 221 + .../pybind11_qt/details/pybind11_qt_utils.h | 47 + .../pybind11-qt/include/pybind11_qt/pybind11_qt.h | 88 + .../include/pybind11_qt/pybind11_qt_basic.h | 36 + .../include/pybind11_qt/pybind11_qt_containers.h | 51 + .../include/pybind11_qt/pybind11_qt_enums.h | 115 + .../include/pybind11_qt/pybind11_qt_holder.h | 59 + .../include/pybind11_qt/pybind11_qt_objects.h | 62 + .../include/pybind11_qt/pybind11_qt_qflags.h | 56 + .../src/pybind11-qt/pybind11_qt_basic.cpp | 156 + .../src/pybind11-qt/pybind11_qt_sip.cpp | 105 + .../src/pybind11-qt/pybind11_qt_utils.cpp | 11 + .../src/pybind11-utils/CMakeLists.txt | 23 + libs/plugin_python/src/pybind11-utils/README.md | 50 + .../src/pybind11-utils/functional.cpp | 28 + .../include/pybind11_utils/functional.h | 155 + .../include/pybind11_utils/generator.h | 57 + .../include/pybind11_utils/shared_cpp_owner.h | 90 + .../include/pybind11_utils/smart_variant.h | 53 + .../include/pybind11_utils/smart_variant_wrapper.h | 180 + libs/plugin_python/src/runner/CMakeLists.txt | 34 + libs/plugin_python/src/runner/error.h | 82 + libs/plugin_python/src/runner/pythonrunner.cpp | 415 + libs/plugin_python/src/runner/pythonrunner.h | 53 + libs/plugin_python/src/runner/pythonutils.cpp | 155 + libs/plugin_python/src/runner/pythonutils.h | 25 + libs/plugin_python/tests/CMakeLists.txt | 4 + libs/plugin_python/tests/mocks/DummyFileTree.h | 34 + libs/plugin_python/tests/mocks/MockOrganizer.h | 65 + libs/plugin_python/tests/python/CMakeLists.txt | 75 + libs/plugin_python/tests/python/conftest.py | 12 + .../tests/python/test_argument_wrapper.cpp | 61 + .../tests/python/test_argument_wrapper.py | 62 + libs/plugin_python/tests/python/test_filetree.cpp | 22 + libs/plugin_python/tests/python/test_filetree.py | 86 + .../plugin_python/tests/python/test_functional.cpp | 38 + libs/plugin_python/tests/python/test_functional.py | 55 + .../tests/python/test_guessed_string.cpp | 34 + .../tests/python/test_guessed_string.py | 36 + libs/plugin_python/tests/python/test_organizer.cpp | 46 + libs/plugin_python/tests/python/test_organizer.py | 14 + .../tests/python/test_path_wrappers.py | 45 + libs/plugin_python/tests/python/test_qt.cpp | 155 + libs/plugin_python/tests/python/test_qt.py | 119 + .../plugin_python/tests/python/test_qt_widgets.cpp | 85 + libs/plugin_python/tests/python/test_qt_widgets.py | 62 + .../tests/python/test_shared_cpp_owner.cpp | 67 + .../tests/python/test_shared_cpp_owner.py | 72 + libs/plugin_python/tests/runner/CMakeLists.txt | 65 + .../tests/runner/plugins/dummy-diagnose.py | 37 + .../tests/runner/plugins/dummy-filemapper.py | 32 + .../tests/runner/plugins/dummy-game.py | 45 + .../tests/runner/plugins/dummy-installer.py | 46 + .../tests/runner/plugins/dummy-iplugin.py | 29 + libs/plugin_python/tests/runner/test_diagnose.cpp | 61 + .../plugin_python/tests/runner/test_filemapper.cpp | 64 + libs/plugin_python/tests/runner/test_game.cpp | 28 + libs/plugin_python/tests/runner/test_installer.cpp | 112 + libs/plugin_python/tests/runner/test_iplugin.cpp | 43 + libs/plugin_python/tests/runner/test_lifetime.cpp | 47 + libs/plugin_python/typings/generate.py | 38 + .../typings/mobase_tests/argument_wrapper.pyi | 27 + .../typings/mobase_tests/filetree.pyi | 9 + .../typings/mobase_tests/functional.pyi | 19 + .../typings/mobase_tests/guessed_string.pyi | 16 + .../typings/mobase_tests/organizer.pyi | 7 + libs/plugin_python/typings/mobase_tests/qt.pyi | 109 + .../typings/mobase_tests/qt_widgets.pyi | 34 + .../typings/mobase_tests/shared_cpp_owner.pyi | 22 + libs/plugin_python/vcpkg.json | 34 + libs/preview_base/.clang-format | 41 + libs/preview_base/.git-blame-ignore-revs | 1 + libs/preview_base/.gitattributes | 7 + libs/preview_base/.github/workflows/build.yml | 20 + libs/preview_base/.github/workflows/linting.yml | 16 + libs/preview_base/.gitignore | 5 + libs/preview_base/.pre-commit-config.yaml | 20 + libs/preview_base/CMakeLists.txt | 5 + libs/preview_base/CMakePresets.json | 67 + libs/preview_base/src/CMakeLists.txt | 7 + libs/preview_base/src/preview_base_en.ts | 27 + libs/preview_base/src/previewbase.cpp | 185 + libs/preview_base/src/previewbase.h | 68 + libs/preview_base/vcpkg.json | 23 + libs/preview_bsa/.clang-format | 41 + libs/preview_bsa/.gitattributes | 7 + libs/preview_bsa/.github/workflows/build.yml | 20 + libs/preview_bsa/.github/workflows/linting.yml | 16 + libs/preview_bsa/.gitignore | 5 + libs/preview_bsa/.pre-commit-config.yaml | 20 + libs/preview_bsa/CMakeLists.txt | 4 + libs/preview_bsa/CMakePresets.json | 67 + libs/preview_bsa/src/CMakeLists.txt | 13 + libs/preview_bsa/src/preview_bsa_en.ts | 45 + libs/preview_bsa/src/previewbsa.cpp | 234 + libs/preview_bsa/src/previewbsa.h | 69 + libs/preview_bsa/src/previewbsa.json | 1 + libs/preview_bsa/src/simplefiletreeitem.cpp | 72 + libs/preview_bsa/src/simplefiletreeitem.h | 32 + libs/preview_bsa/src/simplefiletreemodel.cpp | 150 + libs/preview_bsa/src/simplefiletreemodel.h | 48 + libs/preview_bsa/vcpkg.json | 37 + libs/preview_dds/.editorconfig | 9 + libs/preview_dds/.gitignore | 109 + libs/preview_dds/.pre-commit-config.yaml | 15 + libs/preview_dds/CMakeLists.txt | 5 + libs/preview_dds/CMakePresets.json | 11 + libs/preview_dds/LICENSE | 674 ++ libs/preview_dds/README.md | 2 + libs/preview_dds/src/CMakeLists.txt | 6 + libs/preview_dds/src/DDS/DDSDefinitions.py | 808 ++ libs/preview_dds/src/DDS/DDSFile.py | 220 + libs/preview_dds/src/DDS/__init__.py | 0 libs/preview_dds/src/DDS/glstuff.py | 205 + libs/preview_dds/src/DDSPreview.py | 528 + libs/preview_dds/src/DDSPreview_en.ts | 128 + libs/preview_dds/vcpkg.json | 15 + libs/script_extender_plugin_checker/.editorconfig | 5 + libs/script_extender_plugin_checker/.gitignore | 109 + .../.pre-commit-config.yaml | 15 + libs/script_extender_plugin_checker/CMakeLists.txt | 4 + .../CMakePresets.json | 17 + libs/script_extender_plugin_checker/LICENSE | 674 ++ .../src/CMakeLists.txt | 6 + .../src/ScriptExtenderPluginChecker.py | 318 + .../src/ScriptExtenderPluginChecker_en.ts | 138 + libs/script_extender_plugin_checker/vcpkg.json | 15 + libs/tool_inibakery/.clang-format | 41 + libs/tool_inibakery/.gitattributes | 7 + libs/tool_inibakery/.gitignore | 5 + libs/tool_inibakery/.pre-commit-config.yaml | 20 + libs/tool_inibakery/CMakeLists.txt | 5 + libs/tool_inibakery/CMakePresets.json | 60 + libs/tool_inibakery/src/CMakeLists.txt | 13 + libs/tool_inibakery/src/inibakery.cpp | 97 + libs/tool_inibakery/src/inibakery.h | 58 + libs/tool_inibakery/src/inibakery_en.ts | 17 + libs/tool_inibakery/vcpkg.json | 15 + libs/tool_inieditor/.clang-format | 41 + libs/tool_inieditor/.gitattributes | 7 + libs/tool_inieditor/.gitignore | 5 + libs/tool_inieditor/.pre-commit-config.yaml | 20 + libs/tool_inieditor/CMakeLists.txt | 5 + libs/tool_inieditor/CMakePresets.json | 60 + libs/tool_inieditor/src/CMakeLists.txt | 13 + libs/tool_inieditor/src/inieditor.cpp | 152 + libs/tool_inieditor/src/inieditor.h | 55 + libs/tool_inieditor/src/inieditor.qrc | 5 + libs/tool_inieditor/src/inieditor_en.ts | 54 + .../src/resources/document-properties.png | Bin 0 -> 688 bytes libs/tool_inieditor/vcpkg.json | 15 + libs/uibase/.clang-format | 41 + libs/uibase/.git-blame-ignore-revs | 1 + libs/uibase/.gitattributes | 7 + libs/uibase/.github/workflows/build.yml | 79 + libs/uibase/.github/workflows/linting.yml | 17 + libs/uibase/.gitignore | 9 + libs/uibase/.pre-commit-config.yaml | 20 + libs/uibase/CMakeLists.txt | 34 + libs/uibase/CMakePresets.json | 73 + libs/uibase/README.md | 56 + libs/uibase/cmake/config.cmake.in | 13 + libs/uibase/include/uibase/delayedfilewriter.h | 74 + libs/uibase/include/uibase/diagnosisreport.h | 51 + libs/uibase/include/uibase/dllimport.h | 46 + libs/uibase/include/uibase/errorcodes.h | 21 + libs/uibase/include/uibase/eventfilter.h | 45 + libs/uibase/include/uibase/exceptions.h | 58 + libs/uibase/include/uibase/executableinfo.h | 67 + libs/uibase/include/uibase/expanderwidget.h | 63 + libs/uibase/include/uibase/filemapping.h | 17 + libs/uibase/include/uibase/filesystemutilities.h | 36 + libs/uibase/include/uibase/filterwidget.h | 148 + libs/uibase/include/uibase/finddialog.h | 80 + libs/uibase/include/uibase/formatters.h | 5 + libs/uibase/include/uibase/formatters/enums.h | 16 + libs/uibase/include/uibase/formatters/qt.h | 87 + libs/uibase/include/uibase/formatters/strings.h | 89 + .../include/uibase/game_features/bsainvalidation.h | 26 + .../include/uibase/game_features/dataarchives.h | 35 + .../include/uibase/game_features/game_feature.h | 40 + .../include/uibase/game_features/gameplugins.h | 25 + .../include/uibase/game_features/igamefeatures.h | 166 + .../include/uibase/game_features/localsavegames.h | 21 + .../include/uibase/game_features/moddatachecker.h | 66 + .../include/uibase/game_features/moddatacontent.h | 122 + .../include/uibase/game_features/savegameinfo.h | 52 + .../include/uibase/game_features/scriptextender.h | 53 + .../include/uibase/game_features/unmanagedmods.h | 45 + libs/uibase/include/uibase/guessedvalue.h | 252 + libs/uibase/include/uibase/idownloadmanager.h | 102 + libs/uibase/include/uibase/iexecutable.h | 84 + libs/uibase/include/uibase/iexecutableslist.h | 75 + libs/uibase/include/uibase/ifiletree.h | 1130 +++ libs/uibase/include/uibase/ifiletree_utils.h | 82 + libs/uibase/include/uibase/iinstallationmanager.h | 128 + libs/uibase/include/uibase/iinstance.h | 61 + libs/uibase/include/uibase/iinstancemanager.h | 66 + libs/uibase/include/uibase/imodinterface.h | 343 + libs/uibase/include/uibase/imodlist.h | 229 + libs/uibase/include/uibase/imodrepositorybridge.h | 219 + libs/uibase/include/uibase/imoinfo.h | 616 ++ libs/uibase/include/uibase/iplugin.h | 146 + libs/uibase/include/uibase/iplugindiagnose.h | 103 + libs/uibase/include/uibase/ipluginfilemapper.h | 49 + libs/uibase/include/uibase/iplugingame.h | 380 + libs/uibase/include/uibase/iplugingamefeatures.h | 61 + libs/uibase/include/uibase/iplugininstaller.h | 157 + .../uibase/include/uibase/iplugininstallercustom.h | 79 + .../uibase/include/uibase/iplugininstallersimple.h | 69 + libs/uibase/include/uibase/ipluginlist.h | 268 + libs/uibase/include/uibase/ipluginmodpage.h | 92 + libs/uibase/include/uibase/ipluginpreview.h | 72 + libs/uibase/include/uibase/ipluginproxy.h | 83 + libs/uibase/include/uibase/iplugintool.h | 87 + libs/uibase/include/uibase/iprofile.h | 41 + libs/uibase/include/uibase/isavegame.h | 58 + libs/uibase/include/uibase/isavegameinfowidget.h | 30 + libs/uibase/include/uibase/json.h | 95 + libs/uibase/include/uibase/lineeditclear.h | 40 + libs/uibase/include/uibase/linklabel.h | 38 + libs/uibase/include/uibase/log.h | 346 + libs/uibase/include/uibase/memoizedlock.h | 61 + libs/uibase/include/uibase/moassert.h | 36 + libs/uibase/include/uibase/modrepositoryfileinfo.h | 56 + libs/uibase/include/uibase/nxmurl.h | 87 + libs/uibase/include/uibase/pluginrequirements.h | 193 + libs/uibase/include/uibase/pluginsetting.h | 50 + libs/uibase/include/uibase/questionboxmemory.h | 112 + libs/uibase/include/uibase/registry.h | 39 + libs/uibase/include/uibase/report.h | 117 + libs/uibase/include/uibase/safewritefile.h | 72 + libs/uibase/include/uibase/scopeguard.h | 296 + libs/uibase/include/uibase/sortabletreewidget.h | 64 + libs/uibase/include/uibase/steamutility.h | 47 + libs/uibase/include/uibase/strings.h | 22 + libs/uibase/include/uibase/taskprogressmanager.h | 60 + libs/uibase/include/uibase/textviewer.h | 97 + libs/uibase/include/uibase/tutorabledialog.h | 65 + libs/uibase/include/uibase/tutorialcontrol.h | 83 + libs/uibase/include/uibase/tutorialmanager.h | 109 + libs/uibase/include/uibase/utility.h | 443 + libs/uibase/include/uibase/versioninfo.h | 185 + libs/uibase/include/uibase/versioning.h | 181 + libs/uibase/include/uibase/widgetutility.h | 22 + libs/uibase/include/uibase/windows_compat.h | 1 + libs/uibase/src/CMakeLists.txt | 201 + libs/uibase/src/delayedfilewriter.cpp | 49 + libs/uibase/src/diagnosisreport.cpp | 25 + libs/uibase/src/errorcodes.cpp | 2361 +++++ libs/uibase/src/eventfilter.cpp | 34 + libs/uibase/src/executableinfo.cpp | 105 + libs/uibase/src/expanderwidget.cpp | 93 + libs/uibase/src/filesystemutilities.cpp | 68 + libs/uibase/src/filterwidget.cpp | 693 ++ libs/uibase/src/finddialog.cpp | 51 + libs/uibase/src/finddialog.ui | 76 + libs/uibase/src/guessedvalue.cpp | 1 + libs/uibase/src/ifiletree.cpp | 1029 ++ libs/uibase/src/imodrepositorybridge.cpp | 1 + libs/uibase/src/imoinfo.cpp | 23 + libs/uibase/src/json.cpp | 557 + libs/uibase/src/lineeditclear.cpp | 50 + libs/uibase/src/linklabel.cpp | 27 + libs/uibase/src/log.cpp | 411 + libs/uibase/src/modrepositoryfileinfo.cpp | 81 + libs/uibase/src/nxmurl.cpp | 44 + libs/uibase/src/pch.cpp | 1 + libs/uibase/src/pch.h | 139 + libs/uibase/src/pluginrequirements.cpp | 138 + libs/uibase/src/questionboxmemory.cpp | 200 + libs/uibase/src/questionboxmemory.ui | 171 + libs/uibase/src/registry.cpp | 189 + libs/uibase/src/report.cpp | 453 + libs/uibase/src/safewritefile.cpp | 84 + libs/uibase/src/scopeguard.cpp | 6 + libs/uibase/src/sortabletreewidget.cpp | 116 + libs/uibase/src/steamutility.cpp | 108 + libs/uibase/src/strings.cpp | 46 + libs/uibase/src/taskdialog.ui | 359 + libs/uibase/src/taskprogressmanager.cpp | 125 + libs/uibase/src/textviewer.cpp | 278 + libs/uibase/src/textviewer.ui | 55 + libs/uibase/src/tutorabledialog.cpp | 40 + libs/uibase/src/tutorialcontrol.cpp | 384 + libs/uibase/src/tutorialmanager.cpp | 112 + libs/uibase/src/uibase_en.ts | 407 + libs/uibase/src/utility.cpp | 1260 +++ libs/uibase/src/version.rc | 37 + libs/uibase/src/versioninfo.cpp | 432 + libs/uibase/src/versioning.cpp | 278 + libs/uibase/src/widgetutility.cpp | 59 + libs/uibase/tests/CMakeLists.txt | 13 + libs/uibase/tests/cmake/CMakeLists.txt | 10 + libs/uibase/tests/cmake/plugin.cpp | 4 + libs/uibase/tests/test_formatters.cpp | 48 + libs/uibase/tests/test_ifiletree.cpp | 1172 +++ libs/uibase/tests/test_main.cpp | 15 + libs/uibase/tests/test_strings.cpp | 47 + libs/uibase/tests/test_versioning.cpp | 90 + libs/uibase/tests/translations/tests_en.qm | Bin 0 -> 141 bytes libs/uibase/tests/translations/tests_en.ts | 11 + libs/uibase/tests/translations/tests_fr.qm | Bin 0 -> 147 bytes libs/uibase/tests/translations/tests_fr.ts | 11 + libs/uibase/vcpkg.json | 34 + libs/usvfs/.clang-format | 41 + libs/usvfs/.git-blame-ignore-revs | 1 + libs/usvfs/.gitattributes | 7 + libs/usvfs/.github/workflows/build.yml | 173 + libs/usvfs/.github/workflows/linting.yml | 17 + libs/usvfs/.gitignore | 34 + libs/usvfs/.pre-commit-config.yaml | 20 + libs/usvfs/CMakeLists.txt | 75 + libs/usvfs/CMakePresets.json | 102 + libs/usvfs/LICENSE | 699 ++ libs/usvfs/README.md | 59 + libs/usvfs/cmake/config.cmake.in | 6 + libs/usvfs/include/usvfs/dllimport.h | 31 + libs/usvfs/include/usvfs/logging.h | 29 + libs/usvfs/include/usvfs/sharedparameters.h | 112 + libs/usvfs/include/usvfs/usvfs.h | 232 + libs/usvfs/include/usvfs/usvfs_version.h | 30 + libs/usvfs/include/usvfs/usvfsparameters.h | 68 + libs/usvfs/include/usvfs/usvfsparametersprivate.h | 32 + libs/usvfs/licenses/asmjit.txt | 18 + libs/usvfs/licenses/boost.txt | 23 + libs/usvfs/licenses/cppformat.txt | 9 + libs/usvfs/licenses/googletest.txt | 28 + libs/usvfs/licenses/qt.txt | 174 + libs/usvfs/licenses/spdlog.txt | 21 + libs/usvfs/licenses/udis86.txt | 22 + libs/usvfs/src/shared/CMakeLists.txt | 23 + libs/usvfs/src/shared/addrtools.h | 64 + libs/usvfs/src/shared/directory_tree.cpp | 43 + libs/usvfs/src/shared/directory_tree.h | 629 ++ libs/usvfs/src/shared/exceptionex.cpp | 82 + libs/usvfs/src/shared/exceptionex.h | 99 + libs/usvfs/src/shared/formatters.h | 158 + libs/usvfs/src/shared/loghelpers.cpp | 96 + libs/usvfs/src/shared/loghelpers.h | 123 + libs/usvfs/src/shared/ntdll_declarations.cpp | 75 + libs/usvfs/src/shared/ntdll_declarations.h | 612 ++ libs/usvfs/src/shared/pch.cpp | 1 + libs/usvfs/src/shared/pch.h | 87 + libs/usvfs/src/shared/shared_memory.h | 53 + libs/usvfs/src/shared/shmlogger.cpp | 205 + libs/usvfs/src/shared/shmlogger.h | 105 + libs/usvfs/src/shared/stringcast.cpp | 40 + libs/usvfs/src/shared/stringcast.h | 170 + libs/usvfs/src/shared/stringutils.cpp | 146 + libs/usvfs/src/shared/stringutils.h | 57 + libs/usvfs/src/shared/tree_container.h | 630 ++ libs/usvfs/src/shared/unicodestring.cpp | 77 + libs/usvfs/src/shared/unicodestring.h | 110 + libs/usvfs/src/shared/wildcard.cpp | 194 + libs/usvfs/src/shared/wildcard.h | 67 + libs/usvfs/src/shared/winapi.cpp | 490 + libs/usvfs/src/shared/winapi.h | 610 ++ libs/usvfs/src/shared/windows_sane.h | 28 + libs/usvfs/src/thooklib/CMakeLists.txt | 14 + libs/usvfs/src/thooklib/asmjit_sane.h | 29 + libs/usvfs/src/thooklib/hooklib.cpp | 740 ++ libs/usvfs/src/thooklib/hooklib.h | 125 + libs/usvfs/src/thooklib/pch.cpp | 1 + libs/usvfs/src/thooklib/ttrampolinepool.cpp | 601 ++ libs/usvfs/src/thooklib/ttrampolinepool.h | 222 + libs/usvfs/src/thooklib/udis86wrapper.cpp | 102 + libs/usvfs/src/thooklib/udis86wrapper.h | 62 + libs/usvfs/src/thooklib/utility.cpp | 86 + libs/usvfs/src/thooklib/utility.h | 35 + libs/usvfs/src/tinjectlib/CMakeLists.txt | 8 + libs/usvfs/src/tinjectlib/asmjit_sane.h | 32 + libs/usvfs/src/tinjectlib/injectlib.cpp | 482 + libs/usvfs/src/tinjectlib/injectlib.h | 50 + libs/usvfs/src/usvfs_dll/CMakeLists.txt | 57 + libs/usvfs/src/usvfs_dll/hookcallcontext.cpp | 112 + libs/usvfs/src/usvfs_dll/hookcallcontext.h | 87 + libs/usvfs/src/usvfs_dll/hookcontext.cpp | 330 + libs/usvfs/src/usvfs_dll/hookcontext.h | 224 + libs/usvfs/src/usvfs_dll/hookmanager.cpp | 333 + libs/usvfs/src/usvfs_dll/hookmanager.h | 81 + .../src/usvfs_dll/hooks/file_information_utils.h | 263 + libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp | 1860 ++++ libs/usvfs/src/usvfs_dll/hooks/kernel32.h | 116 + libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp | 1520 +++ libs/usvfs/src/usvfs_dll/hooks/ntdll.h | 57 + libs/usvfs/src/usvfs_dll/hooks/sharedids.h | 9 + libs/usvfs/src/usvfs_dll/maptracker.h | 691 ++ libs/usvfs/src/usvfs_dll/pch.cpp | 1 + libs/usvfs/src/usvfs_dll/redirectiontree.cpp | 27 + libs/usvfs/src/usvfs_dll/redirectiontree.h | 104 + libs/usvfs/src/usvfs_dll/semaphore.cpp | 57 + libs/usvfs/src/usvfs_dll/semaphore.h | 27 + libs/usvfs/src/usvfs_dll/sharedparameters.cpp | 264 + libs/usvfs/src/usvfs_dll/stringcast_boost.h | 42 + libs/usvfs/src/usvfs_dll/usvfs.cpp | 965 ++ libs/usvfs/src/usvfs_dll/usvfsparameters.cpp | 193 + libs/usvfs/src/usvfs_dll/version.rc | 40 + libs/usvfs/src/usvfs_helper/CMakeLists.txt | 6 + libs/usvfs/src/usvfs_helper/inject.cpp | 193 + libs/usvfs/src/usvfs_helper/inject.h | 51 + libs/usvfs/src/usvfs_proxy/CMakeLists.txt | 15 + libs/usvfs/src/usvfs_proxy/main.cpp | 221 + libs/usvfs/src/usvfs_proxy/version.rc | 40 + libs/usvfs/test/CMakeLists.txt | 74 + .../BasicTest/expected/data/file.txt | 0 .../BasicTest/expected/mods/mod1/docs/doc.txt | 0 .../BasicTest/expected/mods/mod1/empty/.gitkeep | 0 .../BasicTest/expected/mods/mod1/readme.txt | 0 .../BasicTest/source/data/file.txt | 0 .../BasicTest/source/mods/mod1/docs/doc.txt | 0 .../BasicTest/source/mods/mod1/empty/.gitkeep | 0 .../BasicTest/source/mods/mod1/info.txt | 0 .../BasicTest/source/mods/mod1/readme.txt | 0 .../BoostFilesystemTest/expected/data/.gitkeep | 0 .../expected/mods/mod1/docs/doc.txt | 0 .../expected/mods/mod1/docs/subdocs/.gitkeep | 0 .../BoostFilesystemTest/source/data/.gitkeep | 0 .../source/mods/mod1/docs/doc.txt | 0 .../source/mods/mod1/docs/subdocs/.gitkeep | 0 .../RedFileSystemTest/example.cpp | 139 + .../RedFileSystemTest/expected/data/.gitkeep | 0 .../r6/storages/HUDPainter/DEFAULT.json | 11 + .../overwrite/r6/storages/HUDPainter/TEST.json | 1 + .../RedFileSystemTest/source/data/.gitkeep | 0 .../r6/storages/HUDPainter/DEFAULT.json | 11 + .../SkipFilesTest/expected/data/docs/doc.skip | 1 + .../SkipFilesTest/expected/data/file.txt | 0 .../SkipFilesTest/expected/mods/mod1/docs/doc.skip | 1 + .../SkipFilesTest/expected/mods/mod1/docs/doc.txt | 0 .../expected/mods/mod1/empty/.gitkeep | 0 .../SkipFilesTest/expected/mods/mod1/readme.skip | 1 + .../SkipFilesTest/expected/mods/mod1/readme.txt | 0 .../SkipFilesTest/expected/overwrite/readme.skip | 1 + .../SkipFilesTest/source/data/docs/doc.skip | 1 + .../SkipFilesTest/source/data/file.txt | 0 .../SkipFilesTest/source/mods/mod1/docs/doc.skip | 1 + .../SkipFilesTest/source/mods/mod1/docs/doc.txt | 0 .../SkipFilesTest/source/mods/mod1/empty/.gitkeep | 0 .../SkipFilesTest/source/mods/mod1/readme.skip | 1 + .../SkipFilesTest/source/mods/mod1/readme.txt | 0 .../basic/mount.postmortem/rfolder/rcopyme4.txt | 1 + .../basic/mount.postmortem/rfolder/rfile0.txt | 1 + .../mount.postmortem/rfolder/rfiledeletewrite.txt | 1 + .../mount.postmortem/rfolder/rfilerewrite.txt | 1 + .../usvfs_test/basic/mount.postmortem/root0.txt | 1 + .../usvfs_test/basic/mount.postmortem/root0w.txt | 1 + .../usvfs_test/basic/mount.postmortem/root1.txt | 1 + .../usvfs_test/basic/mount.postmortem/root1w.txt | 1 + .../usvfs_test/basic/mount/rfolder/rcopyme4.txt | 1 + .../usvfs_test/basic/mount/rfolder/rfile0.txt | 1 + .../usvfs_test/basic/mount/rfolder/rfiledelete.txt | 1 + .../basic/mount/rfolder/rfiledeletewrite.txt | 1 + .../basic/mount/rfolder/rfileoldname.txt | 1 + .../basic/mount/rfolder/rfilerewrite.txt | 1 + .../test/fixtures/usvfs_test/basic/mount/root0.txt | 1 + .../fixtures/usvfs_test/basic/mount/root0w.txt | 1 + .../test/fixtures/usvfs_test/basic/mount/root1.txt | 1 + .../fixtures/usvfs_test/basic/mount/root1w.txt | 1 + .../source.postmortem/mod1/mfolder1/mfile.txt | 1 + .../source.postmortem/mod1/mfolder1/mfilew.txt | 1 + .../mod1/mfolder3/mdeep3/mfile.txt | 1 + .../basic/source.postmortem/mod1/mod1.txt | 1 + .../basic/source.postmortem/mod1/mod1w.txt | 1 + .../source.postmortem/mod2/mfolder2/mfile.txt | 1 + .../source.postmortem/mod2/mfolder4/mfile.txt | 1 + .../mod2/mfolder4/mfiledeletemove.txt | 1 + .../mod2/mfolder4/mfiledeletemove2p.txt | 1 + .../mod2/mfolder4/mfiledeletewrite.txt | 1 + .../mod2/mfolder4/mfiledeletewrite2p.txt | 1 + .../mod2/mfolder4/mfilemoveover.txt | 1 + .../mod2/mfolder4/mfileoverwrite.txt | 1 + .../mod2/mfolder4/mfilerewrite.txt | 1 + .../basic/source.postmortem/mod2/mod2.txt | 1 + .../mod3/mfolder3/mdeep3/mfile.txt | 1 + .../source.postmortem/mod3/mfolder4/mfile.txt | 1 + .../basic/source.postmortem/mod3/mod3.txt | 1 + .../source.postmortem/mod4/mfolder4/mfile.txt | 1 + .../mod4/mfolder4/mfiledeletemove.txt | 1 + .../mod4/mfolder4/mfiledeletewrite.txt | 1 + .../mod4/mfolder4/mfilemoveover.txt | 1 + .../mod4/mfolder4/mfileoverwrite.txt | 1 + .../mod4/mfolder4/mfilerewrite.txt | 1 + .../basic/source.postmortem/overwrite/.empty | 0 .../overwrite/mfolder1/newfile1.txt | 1 + .../overwrite/mfolder1/newfolder1/newfile1.txt | 1 + .../overwrite/mfolder2/newfile2.txt | 1 + .../overwrite/mfolder3/newfolder3/newfile3.txt | 1 + .../overwrite/mfolder3/newfolder3/newfile3e.txt | 1 + .../overwrite/mfolder4/mfiledeletemove2p.txt | 1 + .../overwrite/mfolder4/mfiledeletewrite2p.txt | 1 + .../mfolder4/newfolder4/d/e/e/p/newfile4.txt | 1 + .../mfolder4/newfolder4/d/e/e/p/newfile4e.txt | 1 + .../mfolder4/newfolder4/d/e/e/p/newfile4enr.txt | 1 + .../mfolder4/newfolder4/d/e/epnewfile4.txt | 1 + .../mfolder4/newfolder4/d/e/epnewfile4r.txt | 1 + .../overwrite/mfolder4/newfolder4p/rcopyme4.txt | 1 + .../overwrite/mfolder4/rcopyme4.txt | 1 + .../overwrite/rfolder/rfilenewname.txt | 1 + .../usvfs_test/basic/source.postmortem/root1.txt | 1 + .../usvfs_test/basic/source.postmortem/root1w.txt | 1 + .../usvfs_test/basic/source.postmortem/root2.txt | 1 + .../usvfs_test/basic/source.postmortem/root2w.txt | 1 + .../basic/source/mod1/mfolder1/mfile.txt | 1 + .../basic/source/mod1/mfolder1/mfilew.txt | 1 + .../basic/source/mod1/mfolder3/mdeep3/mfile.txt | 1 + .../fixtures/usvfs_test/basic/source/mod1/mod1.txt | 1 + .../usvfs_test/basic/source/mod1/mod1w.txt | 1 + .../basic/source/mod2/mfolder2/mfile.txt | 1 + .../basic/source/mod2/mfolder4/mfile.txt | 1 + .../basic/source/mod2/mfolder4/mfiledeletemove.txt | 1 + .../source/mod2/mfolder4/mfiledeletemove2p.txt | 1 + .../source/mod2/mfolder4/mfiledeletewrite.txt | 1 + .../source/mod2/mfolder4/mfiledeletewrite2p.txt | 1 + .../basic/source/mod2/mfolder4/mfilemoveover.txt | 1 + .../basic/source/mod2/mfolder4/mfileoverwrite.txt | 1 + .../basic/source/mod2/mfolder4/mfilerewrite.txt | 1 + .../fixtures/usvfs_test/basic/source/mod2/mod2.txt | 1 + .../basic/source/mod3/mfolder3/mdeep3/mfile.txt | 1 + .../basic/source/mod3/mfolder4/mfile.txt | 1 + .../fixtures/usvfs_test/basic/source/mod3/mod3.txt | 1 + .../basic/source/mod4/mfolder4/mfile.txt | 1 + .../basic/source/mod4/mfolder4/mfiledeletemove.txt | 1 + .../source/mod4/mfolder4/mfiledeletemove2p.txt | 1 + .../source/mod4/mfolder4/mfiledeletewrite.txt | 1 + .../source/mod4/mfolder4/mfiledeletewrite2p.txt | 1 + .../basic/source/mod4/mfolder4/mfilemoveover.txt | 1 + .../basic/source/mod4/mfolder4/mfileoverwrite.txt | 1 + .../basic/source/mod4/mfolder4/mfilerewrite.txt | 1 + .../usvfs_test/basic/source/overwrite/.empty | 0 .../fixtures/usvfs_test/basic/source/root1.txt | 1 + .../fixtures/usvfs_test/basic/source/root1w.txt | 1 + .../fixtures/usvfs_test/basic/source/root2.txt | 1 + .../fixtures/usvfs_test/basic/source/root2w.txt | 1 + .../fixtures/usvfs_test/basic/vfs_mappings.txt | 26 + libs/usvfs/test/gtest_utils/CMakeLists.txt | 11 + libs/usvfs/test/gtest_utils/gtest_utils.cpp | 122 + libs/usvfs/test/gtest_utils/gtest_utils.h | 29 + libs/usvfs/test/shared_test/CMakeLists.txt | 7 + libs/usvfs/test/shared_test/main.cpp | 248 + libs/usvfs/test/test_utils/CMakeLists.txt | 11 + libs/usvfs/test/test_utils/test_helpers.cpp | 248 + libs/usvfs/test/test_utils/test_helpers.h | 165 + libs/usvfs/test/thooklib_test/CMakeLists.txt | 11 + libs/usvfs/test/thooklib_test/main.cpp | 235 + libs/usvfs/test/thooklib_test/test_hooks.cpp | 38 + libs/usvfs/test/tinjectlib_test/CMakeLists.txt | 16 + libs/usvfs/test/tinjectlib_test/main.cpp | 158 + .../test/tinjectlib_test/testinject_bin/main.cpp | 4 + .../test/tinjectlib_test/testinject_dll/main.cpp | 36 + .../test/tinjectlib_test/testinject_dll/main.h | 11 + libs/usvfs/test/tvfs_test/CMakeLists.txt | 12 + libs/usvfs/test/tvfs_test/main.cpp | 585 ++ .../test/usvfs_global_test_runner/CMakeLists.txt | 23 + .../usvfs_global_test/usvfs_global_test.cpp | 216 + .../usvfs_global_test_fixture.cpp | 205 + .../usvfs_global_test_fixture.h | 82 + .../usvfs_global_test_runner.cpp | 58 + libs/usvfs/test/usvfs_test_runner/CMakeLists.txt | 35 + .../test_file_operations/test_file_operations.cpp | 496 + .../test_file_operations/test_filesystem.cpp | 159 + .../test_file_operations/test_filesystem.h | 104 + .../test_file_operations/test_ntapi.cpp | 488 + .../test_file_operations/test_ntapi.h | 38 + .../test_file_operations/test_ntdll_declarations.h | 157 + .../test_file_operations/test_w32api.cpp | 370 + .../test_file_operations/test_w32api.h | 36 + .../usvfs_test/usvfs_basic_test.cpp | 402 + .../usvfs_test/usvfs_basic_test.h | 14 + .../usvfs_test_runner/usvfs_test/usvfs_test.cpp | 242 + .../usvfs_test/usvfs_test_base.cpp | 1002 ++ .../usvfs_test_runner/usvfs_test/usvfs_test_base.h | 120 + .../test/usvfs_test_runner/usvfs_test_runner.cpp | 105 + libs/usvfs/vcpkg-configuration.json | 21 + libs/usvfs/vcpkg.json | 24 + libs/vcpkg-registry/.gitattributes | 7 + libs/vcpkg-registry/.pre-commit-config.yaml | 15 + libs/vcpkg-registry/README.md | 57 + .../vcpkg-registry/ports/7zip/7zip-config.cmake.in | 5 + libs/vcpkg-registry/ports/7zip/portfile.cmake | 33 + libs/vcpkg-registry/ports/7zip/vcpkg.json | 18 + libs/vcpkg-registry/ports/asmjit/asmjit.patch | 906 ++ libs/vcpkg-registry/ports/asmjit/portfile.cmake | 31 + libs/vcpkg-registry/ports/asmjit/vcpkg.json | 17 + libs/vcpkg-registry/ports/libloot/portfile.cmake | 30 + libs/vcpkg-registry/ports/libloot/vcpkg.json | 14 + .../ports/mo2-archive/portfile.cmake | 20 + libs/vcpkg-registry/ports/mo2-archive/vcpkg.json | 19 + libs/vcpkg-registry/ports/mo2-bsatk/portfile.cmake | 15 + libs/vcpkg-registry/ports/mo2-bsatk/vcpkg.json | 23 + libs/vcpkg-registry/ports/mo2-cmake/portfile.cmake | 19 + libs/vcpkg-registry/ports/mo2-cmake/vcpkg.json | 8 + .../mo2-dds-header/mo2-dds-header-config.cmake | 10 + .../ports/mo2-dds-header/portfile.cmake | 8 + .../vcpkg-registry/ports/mo2-dds-header/vcpkg.json | 14 + libs/vcpkg-registry/ports/mo2-esptk/portfile.cmake | 15 + libs/vcpkg-registry/ports/mo2-esptk/vcpkg.json | 18 + .../ports/mo2-libbsarch/portfile.cmake | 42 + libs/vcpkg-registry/ports/mo2-libbsarch/vcpkg.json | 19 + .../ports/mo2-lootcli-header/portfile.cmake | 14 + .../ports/mo2-lootcli-header/vcpkg.json | 18 + .../vcpkg-registry/ports/mo2-uibase/portfile.cmake | 47 + libs/vcpkg-registry/ports/mo2-uibase/vcpkg.json | 14 + libs/vcpkg-registry/ports/pybind11/portfile.cmake | 23 + libs/vcpkg-registry/ports/pybind11/vcpkg.json | 18 + libs/vcpkg-registry/ports/spdlog/portfile.cmake | 44 + libs/vcpkg-registry/ports/spdlog/vcpkg.json | 17 + libs/vcpkg-registry/ports/usvfs/portfile.cmake | 95 + libs/vcpkg-registry/ports/usvfs/vcpkg.json | 14 + libs/vcpkg-registry/versions/7-/7zip.json | 14 + libs/vcpkg-registry/versions/a-/asmjit.json | 9 + libs/vcpkg-registry/versions/baseline.json | 60 + libs/vcpkg-registry/versions/l-/libloot.json | 19 + libs/vcpkg-registry/versions/m-/mo2-archive.json | 14 + libs/vcpkg-registry/versions/m-/mo2-bsatk.json | 19 + libs/vcpkg-registry/versions/m-/mo2-cmake.json | 49 + .../vcpkg-registry/versions/m-/mo2-dds-header.json | 9 + libs/vcpkg-registry/versions/m-/mo2-esptk.json | 14 + libs/vcpkg-registry/versions/m-/mo2-libbsarch.json | 24 + .../versions/m-/mo2-lootcli-header.json | 14 + libs/vcpkg-registry/versions/m-/mo2-uibase.json | 39 + libs/vcpkg-registry/versions/p-/pybind11.json | 14 + libs/vcpkg-registry/versions/s-/spdlog.json | 14 + libs/vcpkg-registry/versions/u-/usvfs.json | 14 + libs/winreg.py | 14 + src/fluorine-manager | 4 + src/src/CMakeLists.txt | 266 + src/src/aboutdialog.cpp | 126 + src/src/aboutdialog.h | 82 + src/src/aboutdialog.ui | 727 ++ src/src/activatemodsdialog.cpp | 96 + src/src/activatemodsdialog.h | 78 + src/src/activatemodsdialog.ui | 97 + src/src/apiuseraccount.cpp | 93 + src/src/apiuseraccount.h | 140 + src/src/app_icon.rc | 1 + src/src/archivefiletree.cpp | 280 + src/src/archivefiletree.h | 71 + src/src/bbcode.cpp | 318 + src/src/bbcode.h | 39 + src/src/browserdialog.cpp | 289 + src/src/browserdialog.h | 121 + src/src/browserdialog.ui | 314 + src/src/browserview.cpp | 77 + src/src/browserview.h | 76 + src/src/categories.cpp | 519 + src/src/categories.h | 275 + src/src/categoriesdialog.cpp | 399 + src/src/categoriesdialog.h | 82 + src/src/categoriesdialog.ui | 241 + src/src/categoriestable.cpp | 77 + src/src/categoriestable.h | 37 + src/src/categoryimportdialog.cpp | 75 + src/src/categoryimportdialog.h | 44 + src/src/categoryimportdialog.ui | 144 + src/src/colortable.cpp | 319 + src/src/colortable.h | 37 + src/src/commandline.cpp | 1156 +++ src/src/commandline.h | 416 + src/src/copyeventfilter.cpp | 49 + src/src/copyeventfilter.h | 41 + src/src/createinstancedialog.cpp | 538 + src/src/createinstancedialog.h | 237 + src/src/createinstancedialog.ui | 1353 +++ src/src/createinstancedialogpages.cpp | 1321 +++ src/src/createinstancedialogpages.h | 631 ++ src/src/credentialsdialog.cpp | 42 + src/src/credentialsdialog.h | 45 + src/src/credentialsdialog.ui | 124 + src/src/csvbuilder.cpp | 254 + src/src/csvbuilder.h | 87 + src/src/datatab.cpp | 195 + src/src/datatab.h | 80 + src/src/directoryrefresher.cpp | 514 + src/src/directoryrefresher.h | 211 + src/src/disableproxyplugindialog.cpp | 34 + src/src/disableproxyplugindialog.h | 44 + src/src/disableproxyplugindialog.ui | 174 + src/src/dlls.manifest.debug.qt6 | 34 + src/src/dlls.manifest.qt6 | 34 + src/src/downloadlist.cpp | 332 + src/src/downloadlist.h | 102 + src/src/downloadlistview.cpp | 511 + src/src/downloadlistview.h | 132 + src/src/downloadmanager.cpp | 2449 +++++ src/src/downloadmanager.h | 647 ++ src/src/downloadmanagerproxy.cpp | 79 + src/src/downloadmanagerproxy.h | 45 + src/src/downloadstab.cpp | 110 + src/src/downloadstab.h | 46 + src/src/editexecutablesdialog.cpp | 905 ++ src/src/editexecutablesdialog.h | 239 + src/src/editexecutablesdialog.ui | 534 + src/src/env.cpp | 1557 +++ src/src/env.h | 313 + src/src/envdump.h | 33 + src/src/envfs.cpp | 722 ++ src/src/envfs.h | 192 + src/src/envmetrics.cpp | 379 + src/src/envmetrics.h | 80 + src/src/envmodule.cpp | 1108 ++ src/src/envmodule.h | 228 + src/src/envsecurity.cpp | 764 ++ src/src/envsecurity.h | 71 + src/src/envshell.cpp | 610 ++ src/src/envshell.h | 115 + src/src/envshortcut.cpp | 460 + src/src/envshortcut.h | 107 + src/src/envwindows.cpp | 305 + src/src/envwindows.h | 111 + src/src/executableslist.cpp | 496 + src/src/executableslist.h | 234 + src/src/executableslistproxy.cpp | 63 + src/src/executableslistproxy.h | 46 + src/src/filedialogmemory.cpp | 83 + src/src/filedialogmemory.h | 51 + src/src/filerenamer.cpp | 204 + src/src/filerenamer.h | 144 + src/src/filetree.cpp | 830 ++ src/src/filetree.h | 76 + src/src/filetreeitem.cpp | 456 + src/src/filetreeitem.h | 257 + src/src/filetreemodel.cpp | 1239 +++ src/src/filetreemodel.h | 178 + src/src/filterlist.cpp | 445 + src/src/filterlist.h | 59 + src/src/fluorineconfig.cpp | 146 + src/src/fluorineconfig.h | 32 + src/src/forcedloaddialog.cpp | 71 + src/src/forcedloaddialog.h | 35 + src/src/forcedloaddialog.ui | 117 + src/src/forcedloaddialogwidget.cpp | 109 + src/src/forcedloaddialogwidget.h | 42 + src/src/forcedloaddialogwidget.ui | 113 + src/src/fuseconnector.cpp | 676 ++ src/src/fuseconnector.h | 92 + src/src/game_features.cpp | 350 + src/src/game_features.h | 117 + src/src/gamefeaturesproxy.cpp | 48 + src/src/gamefeaturesproxy.h | 34 + src/src/genericicondelegate.cpp | 27 + src/src/genericicondelegate.h | 40 + src/src/github.cpp | 204 + src/src/github.h | 108 + src/src/glob_matching.h | 185 + src/src/icondelegate.cpp | 87 + src/src/icondelegate.h | 54 + src/src/iconfetcher.cpp | 165 + src/src/iconfetcher.h | 75 + src/src/installationmanager.cpp | 967 ++ src/src/installationmanager.h | 357 + src/src/instancemanager.cpp | 951 ++ src/src/instancemanager.h | 364 + src/src/instancemanagerdialog.cpp | 736 ++ src/src/instancemanagerdialog.h | 145 + src/src/instancemanagerdialog.ui | 444 + src/src/iuserinterface.h | 32 + src/src/json.h | 179 + src/src/lcdnumber.cpp | 34 + src/src/lcdnumber.h | 35 + src/src/listdialog.cpp | 79 + src/src/listdialog.h | 34 + src/src/listdialog.ui | 103 + src/src/loghighlighter.cpp | 47 + src/src/loghighlighter.h | 43 + src/src/loglist.cpp | 413 + src/src/loglist.h | 91 + src/src/loot.cpp | 943 ++ src/src/loot.h | 144 + src/src/lootdialog.cpp | 294 + src/src/lootdialog.h | 82 + src/src/lootdialog.ui | 230 + src/src/main.cpp | 330 + src/src/mainwindow.cpp | 4053 ++++++++ src/src/mainwindow.h | 495 + src/src/mainwindow.ui | 2054 ++++ src/src/messagedialog.cpp | 112 + src/src/messagedialog.h | 73 + src/src/messagedialog.ui | 94 + src/src/mo_icon.ico | Bin 0 -> 45874 bytes src/src/moapplication.cpp | 868 ++ src/src/moapplication.h | 111 + src/src/modconflicticondelegate.cpp | 148 + src/src/modconflicticondelegate.h | 47 + src/src/modcontenticondelegate.cpp | 60 + src/src/modcontenticondelegate.h | 30 + src/src/modelutils.cpp | 95 + src/src/modelutils.h | 28 + src/src/modflagicondelegate.cpp | 102 + src/src/modflagicondelegate.h | 37 + src/src/modidlineedit.cpp | 20 + src/src/modidlineedit.h | 21 + src/src/modinfo.cpp | 576 ++ src/src/modinfo.h | 1061 ++ src/src/modinfobackup.cpp | 17 + src/src/modinfobackup.h | 57 + src/src/modinfodialog.cpp | 826 ++ src/src/modinfodialog.h | 254 + src/src/modinfodialog.ui | 1452 +++ src/src/modinfodialogcategories.cpp | 138 + src/src/modinfodialogcategories.h | 26 + src/src/modinfodialogconflicts.cpp | 1130 +++ src/src/modinfodialogconflicts.h | 162 + src/src/modinfodialogconflictsmodels.cpp | 311 + src/src/modinfodialogconflictsmodels.h | 115 + src/src/modinfodialogesps.cpp | 371 + src/src/modinfodialogesps.h | 31 + src/src/modinfodialogfiletree.cpp | 563 ++ src/src/modinfodialogfiletree.h | 56 + src/src/modinfodialogfwd.h | 52 + src/src/modinfodialogimages.cpp | 1096 ++ src/src/modinfodialogimages.h | 380 + src/src/modinfodialognexus.cpp | 468 + src/src/modinfodialognexus.h | 74 + src/src/modinfodialogtab.cpp | 263 + src/src/modinfodialogtab.h | 302 + src/src/modinfodialogtextfiles.cpp | 236 + src/src/modinfodialogtextfiles.h | 62 + src/src/modinfoforeign.cpp | 68 + src/src/modinfoforeign.h | 124 + src/src/modinfooverwrite.cpp | 88 + src/src/modinfooverwrite.h | 108 + src/src/modinforegular.cpp | 1043 ++ src/src/modinforegular.h | 529 + src/src/modinfoseparator.cpp | 34 + src/src/modinfoseparator.h | 68 + src/src/modinfowithconflictinfo.cpp | 348 + src/src/modinfowithconflictinfo.h | 177 + src/src/modlist.cpp | 1514 +++ src/src/modlist.h | 422 + src/src/modlistbypriorityproxy.cpp | 416 + src/src/modlistbypriorityproxy.h | 116 + src/src/modlistcontextmenu.cpp | 637 ++ src/src/modlistcontextmenu.h | 115 + src/src/modlistdropinfo.cpp | 124 + src/src/modlistdropinfo.h | 89 + src/src/modlistproxy.cpp | 113 + src/src/modlistproxy.h | 53 + src/src/modlistsortproxy.cpp | 716 ++ src/src/modlistsortproxy.h | 167 + src/src/modlistversiondelegate.cpp | 67 + src/src/modlistversiondelegate.h | 22 + src/src/modlistview.cpp | 1538 +++ src/src/modlistview.h | 352 + src/src/modlistviewactions.cpp | 1496 +++ src/src/modlistviewactions.h | 194 + src/src/moshortcut.cpp | 73 + src/src/moshortcut.h | 52 + src/src/motddialog.cpp | 53 + src/src/motddialog.h | 47 + src/src/motddialog.ui | 52 + src/src/multiprocess.cpp | 137 + src/src/multiprocess.h | 62 + src/src/nexusinterface.cpp | 1311 +++ src/src/nexusinterface.h | 688 ++ src/src/nexusmanualkey.ui | 231 + src/src/noeditdelegate.cpp | 9 + src/src/noeditdelegate.h | 14 + src/src/nxmaccessmanager.cpp | 987 ++ src/src/nxmaccessmanager.h | 285 + src/src/nxmhandler_linux.cpp | 345 + src/src/nxmhandler_linux.h | 52 + src/src/organizer_en.ts | 10084 +++++++++++++++++++ src/src/organizercore.cpp | 2422 +++++ src/src/organizercore.h | 601 ++ src/src/organizerproxy.cpp | 515 + src/src/organizerproxy.h | 167 + src/src/overwriteinfodialog.cpp | 316 + src/src/overwriteinfodialog.h | 150 + src/src/overwriteinfodialog.ui | 100 + src/src/pch.cpp | 1 + src/src/pch.h | 282 + src/src/persistentcookiejar.cpp | 78 + src/src/persistentcookiejar.h | 26 + src/src/plugincontainer.cpp | 1264 +++ src/src/plugincontainer.h | 505 + src/src/pluginlist.cpp | 2104 ++++ src/src/pluginlist.h | 445 + src/src/pluginlistcontextmenu.cpp | 165 + src/src/pluginlistcontextmenu.h | 51 + src/src/pluginlistproxy.cpp | 155 + src/src/pluginlistproxy.h | 62 + src/src/pluginlistsortproxy.cpp | 164 + src/src/pluginlistsortproxy.h | 63 + src/src/pluginlistview.cpp | 420 + src/src/pluginlistview.h | 86 + src/src/previewdialog.cpp | 65 + src/src/previewdialog.h | 40 + src/src/previewdialog.ui | 90 + src/src/previewgenerator.cpp | 78 + src/src/previewgenerator.h | 47 + src/src/problemsdialog.cpp | 118 + src/src/problemsdialog.h | 44 + src/src/problemsdialog.ui | 102 + src/src/processrunner.cpp | 1415 +++ src/src/processrunner.h | 211 + src/src/profile.cpp | 1328 +++ src/src/profile.h | 417 + src/src/profileinputdialog.cpp | 45 + src/src/profileinputdialog.h | 45 + src/src/profileinputdialog.ui | 87 + src/src/profilesdialog.cpp | 426 + src/src/profilesdialog.h | 140 + src/src/profilesdialog.ui | 239 + src/src/protonlauncher.cpp | 514 + src/src/protonlauncher.h | 50 + src/src/proxyutils.h | 50 + src/src/qdirfiletree.cpp | 115 + src/src/qdirfiletree.h | 60 + src/src/qt.conf | 3 + src/src/qtgroupingproxy.cpp | 948 ++ src/src/qtgroupingproxy.h | 153 + src/src/queryoverwritedialog.cpp | 65 + src/src/queryoverwritedialog.h | 64 + src/src/queryoverwritedialog.ui | 141 + src/src/resources.qrc | 117 + src/src/resources/accessories-text-editor.png | Bin 0 -> 574 bytes src/src/resources/application-x-executable.png | Bin 0 -> 1675 bytes src/src/resources/applications-accessories.png | Bin 0 -> 2223 bytes src/src/resources/archive-conflict-loser.png | Bin 0 -> 886 bytes src/src/resources/archive-conflict-mixed.png | Bin 0 -> 969 bytes src/src/resources/archive-conflict-neutral.png | Bin 0 -> 846 bytes src/src/resources/archive-conflict-winner.png | Bin 0 -> 939 bytes src/src/resources/arrange-boxes.png | Bin 0 -> 1634 bytes src/src/resources/badge_1.png | Bin 0 -> 1119 bytes src/src/resources/badge_2.png | Bin 0 -> 1223 bytes src/src/resources/badge_3.png | Bin 0 -> 1346 bytes src/src/resources/badge_4.png | Bin 0 -> 1200 bytes src/src/resources/badge_5.png | Bin 0 -> 1269 bytes src/src/resources/badge_6.png | Bin 0 -> 1244 bytes src/src/resources/badge_7.png | Bin 0 -> 1190 bytes src/src/resources/badge_8.png | Bin 0 -> 1252 bytes src/src/resources/badge_9.png | Bin 0 -> 1259 bytes src/src/resources/badge_more.png | Bin 0 -> 1629 bytes src/src/resources/branch-closed.png | Bin 0 -> 593 bytes src/src/resources/branch-open.png | Bin 0 -> 756 bytes src/src/resources/check.png | Bin 0 -> 395 bytes src/src/resources/combobox-down.png | Bin 0 -> 548 bytes src/src/resources/conflict-hidden.png | Bin 0 -> 231 bytes src/src/resources/conflict-mixed-blue.png | Bin 0 -> 571 bytes src/src/resources/conflict-mixed.png | Bin 0 -> 657 bytes src/src/resources/conflict-overwrite-blue.png | Bin 0 -> 594 bytes src/src/resources/conflict-overwrite.png | Bin 0 -> 683 bytes src/src/resources/conflict-overwritten-blue.png | Bin 0 -> 455 bytes src/src/resources/conflict-overwritten.png | Bin 0 -> 543 bytes src/src/resources/conflict-redundant.png | Bin 0 -> 567 bytes src/src/resources/contact-new.png | Bin 0 -> 1390 bytes src/src/resources/contents/breastplate.png | Bin 0 -> 788 bytes src/src/resources/contents/checkbox-tree.png | Bin 0 -> 532 bytes src/src/resources/contents/config.png | Bin 0 -> 480 bytes src/src/resources/contents/conversation.png | Bin 0 -> 832 bytes src/src/resources/contents/double-quaver.png | Bin 0 -> 640 bytes src/src/resources/contents/empty-chessboard.png | Bin 0 -> 537 bytes src/src/resources/contents/f4-nuclear.png | Bin 0 -> 1733 bytes src/src/resources/contents/facegen.png | Bin 0 -> 246 bytes src/src/resources/contents/feather-and-scroll.png | Bin 0 -> 616 bytes src/src/resources/contents/hand-of-god.png | Bin 0 -> 687 bytes src/src/resources/contents/jigsaw-piece.png | Bin 0 -> 678 bytes src/src/resources/contents/locked-chest.png | Bin 0 -> 801 bytes src/src/resources/contents/lyre.png | Bin 0 -> 783 bytes src/src/resources/contents/media.png | Bin 0 -> 114 bytes src/src/resources/contents/mesh-box.png | Bin 0 -> 165 bytes src/src/resources/contents/tinker.png | Bin 0 -> 742 bytes src/src/resources/contents/usable.png | Bin 0 -> 834 bytes src/src/resources/contents/xedit.png | Bin 0 -> 1796 bytes src/src/resources/dialog-information.png | Bin 0 -> 1204 bytes src/src/resources/dialog-warning.png | Bin 0 -> 1391 bytes src/src/resources/dialog-warning_16.png | Bin 0 -> 603 bytes .../resources/dialog-warning_16.png (warning_16) | Bin 0 -> 603 bytes src/src/resources/document-properties.png | Bin 0 -> 688 bytes src/src/resources/document-save.png | Bin 0 -> 1117 bytes src/src/resources/document-save_32.png | Bin 0 -> 1971 bytes src/src/resources/dynamic-blue-right.png | Bin 0 -> 1236 bytes src/src/resources/edit-clear.png | Bin 0 -> 773 bytes src/src/resources/edit-find-replace.png | Bin 0 -> 1088 bytes src/src/resources/edit-undo.png | Bin 0 -> 1601 bytes src/src/resources/emblem-favorite - 64.png | Bin 0 -> 3710 bytes src/src/resources/emblem-favorite.png | Bin 0 -> 836 bytes src/src/resources/emblem-important.png | Bin 0 -> 717 bytes src/src/resources/emblem-readonly.png | Bin 0 -> 430 bytes src/src/resources/emblem-redundant.png | Bin 0 -> 615 bytes src/src/resources/emblem-unreadable.png | Bin 0 -> 669 bytes src/src/resources/error.png | Bin 0 -> 870 bytes src/src/resources/function.png | Bin 0 -> 2282 bytes src/src/resources/game-warning-16.png | Bin 0 -> 780 bytes src/src/resources/game-warning.png | Bin 0 -> 2528 bytes src/src/resources/go-down.png | Bin 0 -> 937 bytes src/src/resources/go-jump.png | Bin 0 -> 1054 bytes src/src/resources/go-next_16.png | Bin 0 -> 719 bytes src/src/resources/go-previous_16.png | Bin 0 -> 718 bytes src/src/resources/go-up.png | Bin 0 -> 974 bytes src/src/resources/help-browser.png | Bin 0 -> 1359 bytes src/src/resources/help-browser_32.png | Bin 0 -> 2231 bytes src/src/resources/icon-favorite.png | Bin 0 -> 2402 bytes src/src/resources/icons8-checked-checkbox.png | Bin 0 -> 886 bytes .../resources/icons8-indeterminate-checkbox.png | Bin 0 -> 674 bytes src/src/resources/icons8-unchecked-checkbox.png | Bin 0 -> 545 bytes src/src/resources/internet-web-browser.png | Bin 0 -> 2251 bytes src/src/resources/list-add.png | Bin 0 -> 386 bytes src/src/resources/list-remove.png | Bin 0 -> 252 bytes src/src/resources/log-debug.png | Bin 0 -> 1039 bytes src/src/resources/mail-attachment.png | Bin 0 -> 649 bytes src/src/resources/markdown.html | 292 + src/src/resources/media-playback-start.png | Bin 0 -> 961 bytes src/src/resources/mo_icon.png | Bin 0 -> 1011 bytes src/src/resources/multiply-red.png | Bin 0 -> 666 bytes src/src/resources/open-Folder-Icon.png | Bin 0 -> 1434 bytes src/src/resources/package.png | Bin 0 -> 1067 bytes src/src/resources/plugins.png | Bin 0 -> 2340 bytes src/src/resources/preferences-system.png | Bin 0 -> 2129 bytes src/src/resources/process-stop.png | Bin 0 -> 1272 bytes src/src/resources/red-archive-conflict-loser.png | Bin 0 -> 746 bytes src/src/resources/save.svg | 1 + src/src/resources/show.png | Bin 0 -> 1788 bytes src/src/resources/software-update-available.png | Bin 0 -> 740 bytes src/src/resources/start-here.png | Bin 0 -> 1394 bytes src/src/resources/status_active.png | Bin 0 -> 641 bytes src/src/resources/status_awaiting.png | Bin 0 -> 631 bytes src/src/resources/status_inactive.png | Bin 0 -> 597 bytes src/src/resources/switch-instance-icon.png | Bin 0 -> 4333 bytes src/src/resources/symbol-backup.png | Bin 0 -> 561 bytes src/src/resources/system-installer.png | Bin 0 -> 1589 bytes src/src/resources/system-search.png | Bin 0 -> 1267 bytes src/src/resources/system-software-update.png | Bin 0 -> 2216 bytes src/src/resources/tracked.png | Bin 0 -> 2284 bytes src/src/resources/view-refresh.png | Bin 0 -> 1364 bytes src/src/resources/view-refresh_16.png | Bin 0 -> 912 bytes src/src/resources/word-wrap.svg | 1 + src/src/resources/x-office-calendar.png | Bin 0 -> 604 bytes src/src/sanitychecks.cpp | 467 + src/src/sanitychecks.h | 26 + src/src/savestab.cpp | 393 + src/src/savestab.h | 66 + src/src/savetextasdialog.cpp | 49 + src/src/savetextasdialog.h | 32 + src/src/savetextasdialog.ui | 69 + src/src/selectiondialog.cpp | 116 + src/src/selectiondialog.h | 77 + src/src/selectiondialog.ui | 85 + src/src/selfupdater.cpp | 351 + src/src/selfupdater.h | 154 + src/src/serverinfo.cpp | 169 + src/src/serverinfo.h | 68 + src/src/settings.cpp | 2584 +++++ src/src/settings.h | 992 ++ src/src/settingsdialog.cpp | 162 + src/src/settingsdialog.h | 96 + src/src/settingsdialog.ui | 2568 +++++ src/src/settingsdialogdiagnostics.cpp | 113 + src/src/settingsdialogdiagnostics.h | 20 + src/src/settingsdialoggeneral.cpp | 181 + src/src/settingsdialoggeneral.h | 25 + src/src/settingsdialogmodlist.cpp | 85 + src/src/settingsdialogmodlist.h | 26 + src/src/settingsdialognexus.cpp | 469 + src/src/settingsdialognexus.h | 70 + src/src/settingsdialogpaths.cpp | 281 + src/src/settingsdialogpaths.h | 34 + src/src/settingsdialogplugins.cpp | 380 + src/src/settingsdialogplugins.h | 57 + src/src/settingsdialogproton.cpp | 447 + src/src/settingsdialogproton.h | 61 + src/src/settingsdialogtheme.cpp | 81 + src/src/settingsdialogtheme.h | 22 + src/src/settingsdialogworkarounds.cpp | 244 + src/src/settingsdialogworkarounds.h | 49 + src/src/settingsutilities.cpp | 350 + src/src/settingsutilities.h | 272 + src/src/shared/appconfig.cpp | 55 + src/src/shared/appconfig.h | 48 + src/src/shared/appconfig.inc | 27 + src/src/shared/directoryentry.cpp | 993 ++ src/src/shared/directoryentry.h | 285 + src/src/shared/fileentry.cpp | 256 + src/src/shared/fileentry.h | 91 + src/src/shared/fileregister.cpp | 183 + src/src/shared/fileregister.h | 53 + src/src/shared/fileregisterfwd.h | 121 + src/src/shared/filesorigin.cpp | 120 + src/src/shared/filesorigin.h | 68 + src/src/shared/originconnection.cpp | 129 + src/src/shared/originconnection.h | 55 + src/src/shared/util.cpp | 517 + src/src/shared/util.h | 96 + src/src/shared/windows_compat.h | 88 + src/src/shared/windows_error.cpp | 63 + src/src/shared/windows_error.h | 57 + src/src/spawn.cpp | 1323 +++ src/src/spawn.h | 134 + src/src/splash.png | Bin 0 -> 74041 bytes src/src/statusbar.cpp | 234 + src/src/statusbar.h | 68 + src/src/stylesheet_resource.qrc | 7 + src/src/stylesheets/Night Eyes.qss | 702 ++ src/src/stylesheets/Parchment v1.1 by Bob.qss | 607 ++ .../stylesheets/Parchment/checkbox-alt-checked.png | Bin 0 -> 301 bytes .../Parchment/checkbox-alt-unchecked-hover.png | Bin 0 -> 283 bytes .../Parchment/checkbox-alt-unchecked.png | Bin 0 -> 253 bytes .../Parchment/checkbox-checked-disabled.png | Bin 0 -> 309 bytes .../Parchment/checkbox-checked-hover.png | Bin 0 -> 339 bytes src/src/stylesheets/Parchment/checkbox-checked.png | Bin 0 -> 319 bytes .../stylesheets/Parchment/checkbox-disabled.png | Bin 0 -> 259 bytes src/src/stylesheets/Parchment/checkbox-hover.png | Bin 0 -> 339 bytes src/src/stylesheets/Parchment/checkbox.png | Bin 0 -> 259 bytes src/src/stylesheets/dark.qss | 395 + src/src/stylesheets/dracula.qss | 445 + src/src/stylesheets/skyrim.qss | 670 ++ src/src/stylesheets/skyrim/arrow-down.png | Bin 0 -> 218 bytes src/src/stylesheets/skyrim/arrow-left.png | Bin 0 -> 226 bytes src/src/stylesheets/skyrim/arrow-right.png | Bin 0 -> 217 bytes src/src/stylesheets/skyrim/arrow-up.png | Bin 0 -> 219 bytes src/src/stylesheets/skyrim/border-image.png | Bin 0 -> 1757 bytes src/src/stylesheets/skyrim/border-image1.png | Bin 0 -> 199 bytes src/src/stylesheets/skyrim/border-image2.png | Bin 0 -> 209 bytes src/src/stylesheets/skyrim/branch-opened.png | Bin 0 -> 233 bytes src/src/stylesheets/skyrim/button-big-border.png | Bin 0 -> 897 bytes src/src/stylesheets/skyrim/button-border.png | Bin 0 -> 833 bytes .../stylesheets/skyrim/button-checked-border.png | Bin 0 -> 874 bytes .../stylesheets/skyrim/checkbox-alt-checked.png | Bin 0 -> 354 bytes .../skyrim/checkbox-alt-unchecked-hover.png | Bin 0 -> 454 bytes .../stylesheets/skyrim/checkbox-alt-unchecked.png | Bin 0 -> 462 bytes .../skyrim/checkbox-checked-disabled.png | Bin 0 -> 804 bytes .../stylesheets/skyrim/checkbox-checked-hover.png | Bin 0 -> 810 bytes src/src/stylesheets/skyrim/checkbox-checked.png | Bin 0 -> 823 bytes src/src/stylesheets/skyrim/checkbox-disabled.png | Bin 0 -> 570 bytes src/src/stylesheets/skyrim/checkbox-hover.png | Bin 0 -> 581 bytes src/src/stylesheets/skyrim/checkbox.png | Bin 0 -> 590 bytes .../stylesheets/skyrim/context-menu-separator.png | Bin 0 -> 166 bytes src/src/stylesheets/skyrim/progress-bar-border.png | Bin 0 -> 1262 bytes src/src/stylesheets/skyrim/progress-bar-chunk.png | Bin 0 -> 164 bytes src/src/stylesheets/skyrim/radio-checked.png | Bin 0 -> 353 bytes src/src/stylesheets/skyrim/radio-hover.png | Bin 0 -> 489 bytes src/src/stylesheets/skyrim/radio.png | Bin 0 -> 513 bytes src/src/stylesheets/skyrim/scrollbar-down.png | Bin 0 -> 546 bytes .../stylesheets/skyrim/scrollbar-horizontal.png | Bin 0 -> 172 bytes src/src/stylesheets/skyrim/scrollbar-left.png | Bin 0 -> 543 bytes src/src/stylesheets/skyrim/scrollbar-right.png | Bin 0 -> 538 bytes src/src/stylesheets/skyrim/scrollbar-up.png | Bin 0 -> 547 bytes src/src/stylesheets/skyrim/scrollbar-vertical.png | Bin 0 -> 181 bytes src/src/stylesheets/skyrim/separator.png | Bin 0 -> 1355 bytes src/src/stylesheets/skyrim/slider-border.png | Bin 0 -> 725 bytes src/src/stylesheets/skyrim/slider-handle.png | Bin 0 -> 380 bytes src/src/stylesheets/vs15 Dark-Green.qss | 920 ++ src/src/stylesheets/vs15 Dark-Orange.qss | 920 ++ src/src/stylesheets/vs15 Dark-Pink.qss | 920 ++ src/src/stylesheets/vs15 Dark-Purple.qss | 920 ++ src/src/stylesheets/vs15 Dark-Red.qss | 920 ++ src/src/stylesheets/vs15 Dark-Yellow.qss | 920 ++ src/src/stylesheets/vs15 Dark.qss | 920 ++ src/src/stylesheets/vs15/branch-closed.png | Bin 0 -> 193 bytes src/src/stylesheets/vs15/branch-open.png | Bin 0 -> 182 bytes .../stylesheets/vs15/checkbox-check-disabled.png | Bin 0 -> 218 bytes src/src/stylesheets/vs15/checkbox-check.png | Bin 0 -> 229 bytes src/src/stylesheets/vs15/combobox-down.png | Bin 0 -> 183 bytes .../stylesheets/vs15/scrollbar-down-disabled.png | Bin 0 -> 177 bytes src/src/stylesheets/vs15/scrollbar-down-hover.png | Bin 0 -> 177 bytes src/src/stylesheets/vs15/scrollbar-down.png | Bin 0 -> 190 bytes .../stylesheets/vs15/scrollbar-left-disabled.png | Bin 0 -> 175 bytes src/src/stylesheets/vs15/scrollbar-left-hover.png | Bin 0 -> 175 bytes src/src/stylesheets/vs15/scrollbar-left.png | Bin 0 -> 188 bytes .../stylesheets/vs15/scrollbar-right-disabled.png | Bin 0 -> 178 bytes src/src/stylesheets/vs15/scrollbar-right-hover.png | Bin 0 -> 179 bytes src/src/stylesheets/vs15/scrollbar-right.png | Bin 0 -> 192 bytes src/src/stylesheets/vs15/scrollbar-up-disabled.png | Bin 0 -> 174 bytes src/src/stylesheets/vs15/scrollbar-up-hover.png | Bin 0 -> 174 bytes src/src/stylesheets/vs15/scrollbar-up.png | Bin 0 -> 187 bytes src/src/stylesheets/vs15/sort-asc.png | Bin 0 -> 173 bytes src/src/stylesheets/vs15/sort-desc.png | Bin 0 -> 173 bytes src/src/stylesheets/vs15/spinner-down.png | Bin 0 -> 174 bytes src/src/stylesheets/vs15/spinner-up.png | Bin 0 -> 174 bytes src/src/stylesheets/vs15/sub-menu-arrow-hover.png | Bin 0 -> 172 bytes src/src/stylesheets/vs15/sub-menu-arrow.png | Bin 0 -> 186 bytes src/src/syncoverwritedialog.cpp | 176 + src/src/syncoverwritedialog.h | 57 + src/src/syncoverwritedialog.ui | 84 + src/src/systemtraymanager.cpp | 77 + src/src/systemtraymanager.h | 48 + src/src/texteditor.cpp | 550 + src/src/texteditor.h | 163 + src/src/thread_utils.h | 76 + src/src/transfersavesdialog.cpp | 278 + src/src/transfersavesdialog.h | 115 + src/src/transfersavesdialog.ui | 187 + src/src/tutorials/Highlight.qml | 18 + src/src/tutorials/Tooltip.qml | 29 + src/src/tutorials/TooltipArea.qml | 55 + src/src/tutorials/TutorialCanceller.qml | 55 + src/src/tutorials/TutorialDescription.qml | 55 + src/src/tutorials/TutorialOverlay.qml | 80 + src/src/tutorials/tutorial_basics.js | 2 + .../tutorials/tutorial_conflictresolution_main.js | 198 + .../tutorial_conflictresolution_modinfo.js | 23 + src/src/tutorials/tutorial_firststeps.js | 5 + src/src/tutorials/tutorial_firststeps_browser.js | 12 + src/src/tutorials/tutorial_firststeps_main.js | 184 + src/src/tutorials/tutorial_firststeps_modinfo.js | 24 + src/src/tutorials/tutorial_firststeps_settings.js | 29 + src/src/tutorials/tutorial_primer_main.js | 141 + src/src/tutorials/tutorial_window_installer.js | 37 + src/src/tutorials/tutorials.js | 83 + src/src/tutorials/tutorials_installdialog.qml | 7 + src/src/tutorials/tutorials_mainwindow.qml | 7 + src/src/tutorials/tutorials_modinfodialog.qml | 7 + src/src/tutorials/tutorials_nexusdialog.qml | 8 + src/src/tutorials/tutorials_settingsdialog.qml | 7 + src/src/uilocker.cpp | 636 ++ src/src/uilocker.h | 97 + src/src/updatedialog.cpp | 81 + src/src/updatedialog.h | 31 + src/src/updatedialog.ui | 200 + src/src/usvfsconnector.cpp | 342 + src/src/usvfsconnector.h | 102 + src/src/validationprogressdialog.ui | 117 + src/src/version.rc | 37 + src/src/vfs/inodetable.cpp | 99 + src/src/vfs/inodetable.h | 23 + src/src/vfs/mo2filesystem.cpp | 794 ++ src/src/vfs/mo2filesystem.h | 64 + src/src/vfs/overwritemanager.cpp | 178 + src/src/vfs/overwritemanager.h | 34 + src/src/vfs/vfs_helper_main.cpp | 319 + src/src/vfs/vfstree.cpp | 380 + src/src/vfs/vfstree.h | 80 + src/src/viewmarkingscrollbar.cpp | 56 + src/src/viewmarkingscrollbar.h | 24 + src/src/virtualfiletree.cpp | 82 + src/src/virtualfiletree.h | 62 + src/src/wineprefix.cpp | 288 + src/src/wineprefix.h | 38 + 2402 files changed, 302904 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/docker.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100755 build.sh create mode 100644 docker/AppRun.sh create mode 100644 docker/Dockerfile create mode 100755 docker/build-inner.sh create mode 100644 docs/FAQ.md create mode 100644 flatpak/com.fluorine.manager.desktop create mode 100644 flatpak/com.fluorine.manager.metainfo.xml create mode 100644 flatpak/com.fluorine.manager.png create mode 100644 flatpak/com.fluorine.manager.yml create mode 100755 flatpak/flatpak-install.sh create mode 100755 flatpak/fluorine-manager-wrapper.sh create mode 100644 libs/7zip/CMakeLists.txt create mode 100644 libs/archive/.clang-format create mode 100644 libs/archive/.git-blame-ignore-revs create mode 100644 libs/archive/.gitattributes create mode 100644 libs/archive/.github/workflows/build.yml create mode 100644 libs/archive/.github/workflows/linting.yml create mode 100644 libs/archive/.gitignore create mode 100644 libs/archive/.pre-commit-config.yaml create mode 100644 libs/archive/CMakeLists.txt create mode 100644 libs/archive/CMakePresets.json create mode 100644 libs/archive/LICENSE create mode 100644 libs/archive/README.md create mode 100644 libs/archive/cmake/config.cmake.in create mode 100644 libs/archive/include/archive/archive.h create mode 100644 libs/archive/src/CMakeLists.txt create mode 100644 libs/archive/src/archive.cpp create mode 100644 libs/archive/src/compat.h create mode 100644 libs/archive/src/extractcallback.cpp create mode 100644 libs/archive/src/extractcallback.h create mode 100644 libs/archive/src/fileio.cpp create mode 100644 libs/archive/src/fileio.h create mode 100644 libs/archive/src/formatter.h create mode 100644 libs/archive/src/inputstream.cpp create mode 100644 libs/archive/src/inputstream.h create mode 100644 libs/archive/src/instrument.h create mode 100644 libs/archive/src/interfaceguids.cpp create mode 100644 libs/archive/src/library.h create mode 100644 libs/archive/src/multioutputstream.cpp create mode 100644 libs/archive/src/multioutputstream.h create mode 100644 libs/archive/src/opencallback.cpp create mode 100644 libs/archive/src/opencallback.h create mode 100644 libs/archive/src/propertyvariant.cpp create mode 100644 libs/archive/src/propertyvariant.h create mode 100644 libs/archive/src/unknown_impl.h create mode 100644 libs/archive/src/version.rc create mode 100644 libs/archive/thirdparty/C/7zTypes.h create mode 100644 libs/archive/thirdparty/C/7zWindows.h create mode 100644 libs/archive/thirdparty/C/Compiler.h create mode 100644 libs/archive/thirdparty/C/Precomp.h create mode 100644 libs/archive/thirdparty/CPP/7zip/Archive/IArchive.h create mode 100644 libs/archive/thirdparty/CPP/7zip/ICoder.h create mode 100644 libs/archive/thirdparty/CPP/7zip/IDecl.h create mode 100644 libs/archive/thirdparty/CPP/7zip/IPassword.h create mode 100644 libs/archive/thirdparty/CPP/7zip/IProgress.h create mode 100644 libs/archive/thirdparty/CPP/7zip/IStream.h create mode 100644 libs/archive/thirdparty/CPP/7zip/MyVersion.h create mode 100644 libs/archive/thirdparty/CPP/7zip/PropID.h create mode 100644 libs/archive/thirdparty/CPP/Common/Common.h create mode 100644 libs/archive/thirdparty/CPP/Common/Common0.h create mode 100644 libs/archive/thirdparty/CPP/Common/MyGuidDef.h create mode 100644 libs/archive/thirdparty/CPP/Common/MyTypes.h create mode 100644 libs/archive/thirdparty/CPP/Common/MyUnknown.h create mode 100644 libs/archive/thirdparty/CPP/Common/MyWindows.cpp create mode 100644 libs/archive/thirdparty/CPP/Common/MyWindows.h create mode 100644 libs/archive/thirdparty/CPP/Common/NewHandler.h create mode 100644 libs/archive/thirdparty/CPP/Common/StdAfx.h create mode 100644 libs/archive/vcpkg.json create mode 100644 libs/basic_games/.github/workflows/build.yml create mode 100644 libs/basic_games/.github/workflows/linters.yml create mode 100644 libs/basic_games/.gitignore create mode 100644 libs/basic_games/.pre-commit-config.yaml create mode 100644 libs/basic_games/CMakeLists.txt create mode 100644 libs/basic_games/CMakePresets.json create mode 100644 libs/basic_games/LICENSE create mode 100644 libs/basic_games/README.md create mode 100644 libs/basic_games/__init__.py create mode 100644 libs/basic_games/basic_features/__init__.py create mode 100644 libs/basic_games/basic_features/basic_local_savegames.py create mode 100644 libs/basic_games/basic_features/basic_mod_data_checker.py create mode 100644 libs/basic_games/basic_features/basic_save_game_info.py create mode 100644 libs/basic_games/basic_features/utils.py create mode 100644 libs/basic_games/basic_game.py create mode 100644 libs/basic_games/basic_game_ini.py create mode 100644 libs/basic_games/eadesktop_utils.py create mode 100644 libs/basic_games/epic_utils.py create mode 100644 libs/basic_games/games/__init__.py create mode 100644 libs/basic_games/games/baldursgate3/__init__.py create mode 100644 libs/basic_games/games/baldursgate3/bg3_data_checker.py create mode 100644 libs/basic_games/games/baldursgate3/bg3_data_content.py create mode 100644 libs/basic_games/games/baldursgate3/bg3_file_mapper.py create mode 100644 libs/basic_games/games/baldursgate3/bg3_utils.py create mode 100644 libs/basic_games/games/baldursgate3/lslib_retriever.py create mode 100644 libs/basic_games/games/baldursgate3/pak_parser.py create mode 100644 libs/basic_games/games/baldursgate3/plugins/__init__.py create mode 100644 libs/basic_games/games/baldursgate3/plugins/bg3_tool_plugin.py create mode 100644 libs/basic_games/games/baldursgate3/plugins/check_for_lslib_updates_plugin.py create mode 100644 libs/basic_games/games/baldursgate3/plugins/convert_jsons_to_yaml_plugin.py create mode 100644 libs/basic_games/games/baldursgate3/plugins/icons.py create mode 100644 libs/basic_games/games/baldursgate3/plugins/reparse_pak_metadata_plugin.py create mode 100644 libs/basic_games/games/game_arkhamcity.py create mode 100644 libs/basic_games/games/game_assettocorsa.py create mode 100644 libs/basic_games/games/game_baldursgate3.py create mode 100644 libs/basic_games/games/game_blackandwhite2.py create mode 100644 libs/basic_games/games/game_bladeandsorcery.py create mode 100644 libs/basic_games/games/game_borderlands1.py create mode 100644 libs/basic_games/games/game_control.py create mode 100644 libs/basic_games/games/game_cyberpunk2077.py create mode 100644 libs/basic_games/games/game_da2.py create mode 100644 libs/basic_games/games/game_daggerfallunity.py create mode 100644 libs/basic_games/games/game_dao.py create mode 100644 libs/basic_games/games/game_darkestdungeon.py create mode 100644 libs/basic_games/games/game_darkmessiahofmightandmagic.py create mode 100644 libs/basic_games/games/game_darksouls.py create mode 100644 libs/basic_games/games/game_darksouls2sotfs.py create mode 100644 libs/basic_games/games/game_dispatch.py create mode 100644 libs/basic_games/games/game_divinityoriginalsin.py create mode 100644 libs/basic_games/games/game_divinityoriginalsinee.py create mode 100644 libs/basic_games/games/game_dragonsdogmadarkarisen.py create mode 100644 libs/basic_games/games/game_dungeonsiege1.py create mode 100644 libs/basic_games/games/game_dungeonsiege2.py create mode 100644 libs/basic_games/games/game_f123.py create mode 100644 libs/basic_games/games/game_fantasylifei.py create mode 100644 libs/basic_games/games/game_finalfantasy7rebirth.py create mode 100644 libs/basic_games/games/game_finalfantasy7remake.py create mode 100644 libs/basic_games/games/game_gta-3-de.py create mode 100644 libs/basic_games/games/game_gta-san-andreas-de.py create mode 100644 libs/basic_games/games/game_gta-vice-city-de.py create mode 100644 libs/basic_games/games/game_kerbalspaceprogram.py create mode 100644 libs/basic_games/games/game_kingdomcomedeliverance.py create mode 100644 libs/basic_games/games/game_masterduel.py create mode 100644 libs/basic_games/games/game_metalgearsolid2mc.py create mode 100644 libs/basic_games/games/game_metalgearsolid3mc.py create mode 100644 libs/basic_games/games/game_mirrorsedge.py create mode 100644 libs/basic_games/games/game_monsterhunterrise.py create mode 100644 libs/basic_games/games/game_monsterhunterworld.py create mode 100644 libs/basic_games/games/game_mountandblade2.py create mode 100644 libs/basic_games/games/game_msfs2020.py create mode 100644 libs/basic_games/games/game_nfshs.py create mode 100644 libs/basic_games/games/game_nierautomata.py create mode 100644 libs/basic_games/games/game_nomanssky.py create mode 100644 libs/basic_games/games/game_oblivion_remaster.py create mode 100644 libs/basic_games/games/game_readyornot.py create mode 100644 libs/basic_games/games/game_schedule1.py create mode 100644 libs/basic_games/games/game_sekiroshadowsdietwice.py create mode 100644 libs/basic_games/games/game_silenthill2remake.py create mode 100644 libs/basic_games/games/game_silksong.py create mode 100644 libs/basic_games/games/game_sims4.py create mode 100644 libs/basic_games/games/game_stalkeranomaly.py create mode 100644 libs/basic_games/games/game_stardewvalley.py create mode 100644 libs/basic_games/games/game_starsector.py create mode 100644 libs/basic_games/games/game_starwars-empire-at-war-foc.py create mode 100644 libs/basic_games/games/game_starwars-empire-at-war.py create mode 100644 libs/basic_games/games/game_subnautica-below-zero.py create mode 100644 libs/basic_games/games/game_subnautica.py create mode 100644 libs/basic_games/games/game_tdu.py create mode 100644 libs/basic_games/games/game_tdu2.py create mode 100644 libs/basic_games/games/game_thebindingofisaacrebirth.py create mode 100644 libs/basic_games/games/game_thps3.py create mode 100644 libs/basic_games/games/game_thps4.py create mode 100644 libs/basic_games/games/game_thug.py create mode 100644 libs/basic_games/games/game_thug2.py create mode 100644 libs/basic_games/games/game_tmuf.py create mode 100644 libs/basic_games/games/game_trainsimulator.py create mode 100644 libs/basic_games/games/game_valheim.py create mode 100644 libs/basic_games/games/game_valkyriachronicles.py create mode 100644 libs/basic_games/games/game_vampirebloodlines.py create mode 100644 libs/basic_games/games/game_witcher1.py create mode 100644 libs/basic_games/games/game_witcher2.py create mode 100644 libs/basic_games/games/game_witcher3.py create mode 100644 libs/basic_games/games/game_x4.py create mode 100644 libs/basic_games/games/game_xplane11.py create mode 100644 libs/basic_games/games/game_zeusandposeidon.py create mode 100644 libs/basic_games/games/oblivion_remaster/__init__.py create mode 100644 libs/basic_games/games/oblivion_remaster/constants.py create mode 100644 libs/basic_games/games/oblivion_remaster/game_plugins.py create mode 100644 libs/basic_games/games/oblivion_remaster/mod_data_checker.py create mode 100644 libs/basic_games/games/oblivion_remaster/mod_data_content.py create mode 100644 libs/basic_games/games/oblivion_remaster/paks/__init__.py create mode 100644 libs/basic_games/games/oblivion_remaster/paks/model.py create mode 100644 libs/basic_games/games/oblivion_remaster/paks/view.py create mode 100644 libs/basic_games/games/oblivion_remaster/paks/widget.py create mode 100644 libs/basic_games/games/oblivion_remaster/script_extender.py create mode 100644 libs/basic_games/games/oblivion_remaster/ue4ss/__init__.py create mode 100644 libs/basic_games/games/oblivion_remaster/ue4ss/model.py create mode 100644 libs/basic_games/games/oblivion_remaster/ue4ss/view.py create mode 100644 libs/basic_games/games/oblivion_remaster/ue4ss/widget.py create mode 100644 libs/basic_games/games/quarantine/game_masseffectlegendary.py create mode 100644 libs/basic_games/games/quarantine/readme.txt create mode 100644 libs/basic_games/games/stalkeranomaly/XRIO.py create mode 100644 libs/basic_games/games/stalkeranomaly/XRMath.py create mode 100644 libs/basic_games/games/stalkeranomaly/XRNET.py create mode 100644 libs/basic_games/games/stalkeranomaly/XRObject.py create mode 100644 libs/basic_games/games/stalkeranomaly/XRSave.py create mode 100644 libs/basic_games/games/stalkeranomaly/__init__.py create mode 100644 libs/basic_games/gog_utils.py create mode 100644 libs/basic_games/origin_utils.py create mode 100644 libs/basic_games/plugin-requirements.txt create mode 100644 libs/basic_games/poetry.lock create mode 100644 libs/basic_games/pyproject.toml create mode 100644 libs/basic_games/steam_utils.py create mode 100644 libs/basic_games/vcpkg.json create mode 100644 libs/basic_games/winreg.py create mode 100644 libs/bsa_extractor/.gitattributes create mode 100644 libs/bsa_extractor/.github/workflows/build.yml create mode 100644 libs/bsa_extractor/.gitignore create mode 100644 libs/bsa_extractor/.pre-commit-config.yaml create mode 100644 libs/bsa_extractor/CMakeLists.txt create mode 100644 libs/bsa_extractor/CMakePresets.json create mode 100644 libs/bsa_extractor/src/CMakeLists.txt create mode 100644 libs/bsa_extractor/src/bsa_extractor_en.ts create mode 100644 libs/bsa_extractor/src/bsaextractor.cpp create mode 100644 libs/bsa_extractor/src/bsaextractor.h create mode 100644 libs/bsa_extractor/vcpkg.json create mode 100644 libs/bsa_ffi/CMakeLists.txt create mode 100644 libs/bsa_ffi/Cargo.lock create mode 100644 libs/bsa_ffi/Cargo.toml create mode 100644 libs/bsa_ffi/include/bsa_ffi.h create mode 100644 libs/bsa_ffi/src/archive/ba2_reader.rs create mode 100644 libs/bsa_ffi/src/archive/ba2_writer.rs create mode 100644 libs/bsa_ffi/src/archive/mod.rs create mode 100644 libs/bsa_ffi/src/archive/reader.rs create mode 100644 libs/bsa_ffi/src/archive/tes3_reader.rs create mode 100644 libs/bsa_ffi/src/archive/writer.rs create mode 100644 libs/bsa_ffi/src/lib.rs create mode 100644 libs/bsapacker/.gitattributes create mode 100644 libs/bsapacker/.github/workflows/build.yml create mode 100644 libs/bsapacker/.gitignore create mode 100644 libs/bsapacker/.pre-commit-config.yaml create mode 100644 libs/bsapacker/CMakeLists.txt create mode 100644 libs/bsapacker/CMakePresets.json create mode 100644 libs/bsapacker/LICENSE.md create mode 100644 libs/bsapacker/README.md create mode 100644 libs/bsapacker/src/ArchiveAutoService.cpp create mode 100644 libs/bsapacker/src/ArchiveBuildDirector.cpp create mode 100644 libs/bsapacker/src/ArchiveBuilderFactory.cpp create mode 100644 libs/bsapacker/src/ArchiveBuilderHelper.cpp create mode 100644 libs/bsapacker/src/ArchiveNameService.cpp create mode 100644 libs/bsapacker/src/ArchiveNameService.h create mode 100644 libs/bsapacker/src/BSArchive.cpp create mode 100644 libs/bsapacker/src/BSArchiveAuto.cpp create mode 100644 libs/bsapacker/src/BSArchiveEntries.cpp create mode 100644 libs/bsapacker/src/BsaPacker.cpp create mode 100644 libs/bsapacker/src/BsaPacker.h create mode 100644 libs/bsapacker/src/BsaPackerWorker.cpp create mode 100644 libs/bsapacker/src/BsaPackerWorker.h create mode 100644 libs/bsapacker/src/CMakeLists.txt create mode 100644 libs/bsapacker/src/DummyPluginLogic.cpp create mode 100644 libs/bsapacker/src/DummyPluginLogic.h create mode 100644 libs/bsapacker/src/DummyPluginServiceFactory.cpp create mode 100644 libs/bsapacker/src/DummyPluginServiceFactory.h create mode 100644 libs/bsapacker/src/Fallout3DummyPluginService.cpp create mode 100644 libs/bsapacker/src/Fallout3DummyPluginService.h create mode 100644 libs/bsapacker/src/Fallout4DummyPluginService.cpp create mode 100644 libs/bsapacker/src/Fallout4DummyPluginService.h create mode 100644 libs/bsapacker/src/FileWriterService.cpp create mode 100644 libs/bsapacker/src/FileWriterService.h create mode 100644 libs/bsapacker/src/GeneralArchiveBuilder.cpp create mode 100644 libs/bsapacker/src/HideLooseAssetService.cpp create mode 100644 libs/bsapacker/src/HideLooseAssetService.h create mode 100644 libs/bsapacker/src/ModContext.cpp create mode 100644 libs/bsapacker/src/ModContext.h create mode 100644 libs/bsapacker/src/ModDto.cpp create mode 100644 libs/bsapacker/src/ModDto.h create mode 100644 libs/bsapacker/src/ModDtoFactory.cpp create mode 100644 libs/bsapacker/src/NewVegasDummyPluginService.cpp create mode 100644 libs/bsapacker/src/NewVegasDummyPluginService.h create mode 100644 libs/bsapacker/src/NexusId.h create mode 100644 libs/bsapacker/src/NullArchiveBuilder.cpp create mode 100644 libs/bsapacker/src/NullDummyPluginService.cpp create mode 100644 libs/bsapacker/src/NullDummyPluginService.h create mode 100644 libs/bsapacker/src/NullModDto.cpp create mode 100644 libs/bsapacker/src/NullModDto.h create mode 100644 libs/bsapacker/src/OblivionDummyPluginService.cpp create mode 100644 libs/bsapacker/src/OblivionDummyPluginService.h create mode 100644 libs/bsapacker/src/OverrideFileService.cpp create mode 100644 libs/bsapacker/src/OverrideFileService.h create mode 100644 libs/bsapacker/src/PackerDialog.cpp create mode 100644 libs/bsapacker/src/PackerDialog.h create mode 100644 libs/bsapacker/src/PackerDialogFactory.cpp create mode 100644 libs/bsapacker/src/PackerDialogFactory.h create mode 100644 libs/bsapacker/src/SettingsService.cpp create mode 100644 libs/bsapacker/src/SettingsService.h create mode 100644 libs/bsapacker/src/SkyrimDummyPluginService.cpp create mode 100644 libs/bsapacker/src/SkyrimDummyPluginService.h create mode 100644 libs/bsapacker/src/SkyrimSEDummyPluginService.cpp create mode 100644 libs/bsapacker/src/SkyrimSEDummyPluginService.h create mode 100644 libs/bsapacker/src/StarfieldDummyPluginService.cpp create mode 100644 libs/bsapacker/src/StarfieldDummyPluginService.h create mode 100644 libs/bsapacker/src/TextureArchiveBuilder.cpp create mode 100644 libs/bsapacker/src/TexturelessArchiveBuilder.cpp create mode 100644 libs/bsapacker/src/bsa_packer.json create mode 100644 libs/bsapacker/src/bsa_packer_en.ts create mode 100644 libs/bsapacker/src/bsapacker/ArchiveAutoService.h create mode 100644 libs/bsapacker/src/bsapacker/ArchiveBuildDirector.h create mode 100644 libs/bsapacker/src/bsapacker/ArchiveBuilderFactory.h create mode 100644 libs/bsapacker/src/bsapacker/ArchiveBuilderHelper.h create mode 100644 libs/bsapacker/src/bsapacker/GeneralArchiveBuilder.h create mode 100644 libs/bsapacker/src/bsapacker/IArchiveAutoService.h create mode 100644 libs/bsapacker/src/bsapacker/IArchiveBuilder.h create mode 100644 libs/bsapacker/src/bsapacker/IArchiveBuilderFactory.h create mode 100644 libs/bsapacker/src/bsapacker/IArchiveBuilderHelper.h create mode 100644 libs/bsapacker/src/bsapacker/IArchiveNameService.h create mode 100644 libs/bsapacker/src/bsapacker/IDummyPluginLogic.h create mode 100644 libs/bsapacker/src/bsapacker/IDummyPluginService.h create mode 100644 libs/bsapacker/src/bsapacker/IDummyPluginServiceFactory.h create mode 100644 libs/bsapacker/src/bsapacker/IEmitsValueChanged.h create mode 100644 libs/bsapacker/src/bsapacker/IFileWriterService.h create mode 100644 libs/bsapacker/src/bsapacker/IHideLooseAssetService.h create mode 100644 libs/bsapacker/src/bsapacker/IModContext.h create mode 100644 libs/bsapacker/src/bsapacker/IModDto.h create mode 100644 libs/bsapacker/src/bsapacker/IModDtoFactory.h create mode 100644 libs/bsapacker/src/bsapacker/IOverrideFileService.h create mode 100644 libs/bsapacker/src/bsapacker/IPackerDialog.h create mode 100644 libs/bsapacker/src/bsapacker/IPackerDialogFactory.h create mode 100644 libs/bsapacker/src/bsapacker/ISettingsService.h create mode 100644 libs/bsapacker/src/bsapacker/ModDtoFactory.h create mode 100644 libs/bsapacker/src/bsapacker/NullArchiveBuilder.h create mode 100644 libs/bsapacker/src/bsapacker/TextureArchiveBuilder.h create mode 100644 libs/bsapacker/src/bsapacker/TexturelessArchiveBuilder.h create mode 100644 libs/bsapacker/src/bsapacker_global.h create mode 100644 libs/bsapacker/src/qlibbsarch/BSArchive.h create mode 100644 libs/bsapacker/src/qlibbsarch/BSArchiveAuto.h create mode 100644 libs/bsapacker/src/qlibbsarch/BSArchiveEntries.h create mode 100644 libs/bsapacker/src/qlibbsarch/QLibbsarch.h create mode 100644 libs/bsapacker/tests/ArchiveAutoServiceFacts.cpp create mode 100644 libs/bsapacker/tests/ArchiveBuilderFactoryFacts.cpp create mode 100644 libs/bsapacker/tests/ArchiveBuilderHelperFacts.cpp create mode 100644 libs/bsapacker/tests/ArchiveNameServiceFacts.cpp create mode 100644 libs/bsapacker/tests/BsaPackerFacts.cpp create mode 100644 libs/bsapacker/tests/BsaPackerWorkerFacts.cpp create mode 100644 libs/bsapacker/tests/CMakeLists.txt create mode 100644 libs/bsapacker/tests/DummyPluginServiceFactoryFacts.cpp create mode 100644 libs/bsapacker/tests/Fallout3DummyPluginServiceFacts.cpp create mode 100644 libs/bsapacker/tests/Fallout4DummyPluginServiceFacts.cpp create mode 100644 libs/bsapacker/tests/FileWriterServiceFacts.cpp create mode 100644 libs/bsapacker/tests/GeneralArchiveBuilderFacts.cpp create mode 100644 libs/bsapacker/tests/HideLooseAssetServiceFacts.cpp create mode 100644 libs/bsapacker/tests/MockArchiveAutoService.h create mode 100644 libs/bsapacker/tests/MockArchiveBuilderFactory.h create mode 100644 libs/bsapacker/tests/MockArchiveBuilderHelper.h create mode 100644 libs/bsapacker/tests/MockArchiveNameService.h create mode 100644 libs/bsapacker/tests/MockDummyPluginLogic.h create mode 100644 libs/bsapacker/tests/MockDummyPluginServiceFactory.h create mode 100644 libs/bsapacker/tests/MockFileWriterService.h create mode 100644 libs/bsapacker/tests/MockHideLooseAssetService.h create mode 100644 libs/bsapacker/tests/MockModContext.h create mode 100644 libs/bsapacker/tests/MockModDtoFactory.h create mode 100644 libs/bsapacker/tests/MockOrganizer.h create mode 100644 libs/bsapacker/tests/MockOverrideFileService.h create mode 100644 libs/bsapacker/tests/MockPackerDialog.h create mode 100644 libs/bsapacker/tests/MockSettingsService.h create mode 100644 libs/bsapacker/tests/ModContextFacts.cpp create mode 100644 libs/bsapacker/tests/ModDtoFactoryFacts.cpp create mode 100644 libs/bsapacker/tests/NewVegasDummyPluginServiceFacts.cpp create mode 100644 libs/bsapacker/tests/OblivionDummyPluginServiceFacts.cpp create mode 100644 libs/bsapacker/tests/SkyrimDummyPluginServiceFacts.cpp create mode 100644 libs/bsapacker/tests/SkyrimSEDummyPluginServiceFacts.cpp create mode 100644 libs/bsapacker/tests/TextureArchiveBuilderFacts.cpp create mode 100644 libs/bsapacker/tests/TexturelessArchiveBuilderFacts.cpp create mode 100644 libs/bsapacker/vcpkg.json create mode 100644 libs/bsatk/.clang-format create mode 100644 libs/bsatk/.gitattributes create mode 100644 libs/bsatk/.github/workflows/build.yml create mode 100644 libs/bsatk/.github/workflows/linting.yml create mode 100644 libs/bsatk/.gitignore create mode 100644 libs/bsatk/.pre-commit-config.yaml create mode 100644 libs/bsatk/CMakeFiles/CMakeSystem.cmake create mode 100644 libs/bsatk/CMakeLists.txt create mode 100644 libs/bsatk/CMakePresets.json create mode 100644 libs/bsatk/README.md create mode 100644 libs/bsatk/Version.cmake create mode 100644 libs/bsatk/cmake/config.cmake.in create mode 100644 libs/bsatk/include/bsatk/bsaarchive.h create mode 100644 libs/bsatk/include/bsatk/bsaexception.h create mode 100644 libs/bsatk/include/bsatk/bsafile.h create mode 100644 libs/bsatk/include/bsatk/bsafolder.h create mode 100644 libs/bsatk/include/bsatk/bsatk.h create mode 100644 libs/bsatk/include/bsatk/bsatypes.h create mode 100644 libs/bsatk/include/bsatk/errorcodes.h create mode 100644 libs/bsatk/include/bsatk/filehash.h create mode 100644 libs/bsatk/src/CMakeLists.txt create mode 100644 libs/bsatk/src/bsaarchive.cpp create mode 100644 libs/bsatk/src/bsaexception.cpp create mode 100644 libs/bsatk/src/bsafile.cpp create mode 100644 libs/bsatk/src/bsafolder.cpp create mode 100644 libs/bsatk/src/bsatypes.cpp create mode 100644 libs/bsatk/src/filehash.cpp create mode 100644 libs/bsatk/vcpkg.json create mode 100644 libs/check_fnis/.clang-format create mode 100644 libs/check_fnis/.gitattributes create mode 100644 libs/check_fnis/.gitignore create mode 100644 libs/check_fnis/.pre-commit-config.yaml create mode 100644 libs/check_fnis/CMakeLists.txt create mode 100644 libs/check_fnis/CMakePresets.json create mode 100644 libs/check_fnis/src/CMakeLists.txt create mode 100644 libs/check_fnis/src/check_fnis_en.ts create mode 100644 libs/check_fnis/src/checkfnis.cpp create mode 100644 libs/check_fnis/src/checkfnis.h create mode 100644 libs/check_fnis/vcpkg.json create mode 100644 libs/cmake_common/.pre-commit-config.yaml create mode 100644 libs/cmake_common/README.md create mode 100644 libs/cmake_common/mo2-cmake-config.cmake create mode 100644 libs/cmake_common/mo2.cmake create mode 100644 libs/cmake_common/mo2_cpp.cmake create mode 100644 libs/cmake_common/mo2_python.cmake create mode 100644 libs/cmake_common/mo2_utils.cmake create mode 100644 libs/cmake_common/mo2_versions.cmake create mode 100644 libs/dds-header/include/DDS/DDS.h create mode 100644 libs/dds-header/include/DDS/dxgiformat.h create mode 100644 libs/dds-header/mo2-dds-header-config.cmake create mode 100644 libs/dds_header/.editorconfig create mode 100644 libs/dds_header/.gitignore create mode 100644 libs/dds_header/.pre-commit-config.yaml create mode 100644 libs/dds_header/CMakeLists.txt create mode 100644 libs/dds_header/CMakePresets.json create mode 100644 libs/dds_header/LICENSE create mode 100644 libs/dds_header/README.md create mode 100644 libs/dds_header/src/CMakeLists.txt create mode 100644 libs/dds_header/src/DDS/DDSDefinitions.py create mode 100644 libs/dds_header/src/DDS/DDSFile.py create mode 100644 libs/dds_header/src/DDS/__init__.py create mode 100644 libs/dds_header/src/DDS/glstuff.py create mode 100644 libs/dds_header/src/DDSPreview.py create mode 100644 libs/dds_header/src/DDSPreview_en.ts create mode 100644 libs/dds_header/vcpkg.json create mode 100644 libs/diagnose_basic/.clang-format create mode 100644 libs/diagnose_basic/.gitattributes create mode 100644 libs/diagnose_basic/.github/workflows/build.yml create mode 100644 libs/diagnose_basic/.github/workflows/linting.yml create mode 100644 libs/diagnose_basic/.gitignore create mode 100644 libs/diagnose_basic/.pre-commit-config.yaml create mode 100644 libs/diagnose_basic/CMakeLists.txt create mode 100644 libs/diagnose_basic/CMakePresets.json create mode 100644 libs/diagnose_basic/src/CMakeLists.txt create mode 100644 libs/diagnose_basic/src/diagnose_basic_en.ts create mode 100644 libs/diagnose_basic/src/diagnosebasic.cpp create mode 100644 libs/diagnose_basic/src/diagnosebasic.h create mode 100644 libs/diagnose_basic/vcpkg.json create mode 100644 libs/esptk/.clang-format create mode 100644 libs/esptk/.gitattributes create mode 100644 libs/esptk/.github/workflows/build.yml create mode 100644 libs/esptk/.github/workflows/linting.yml create mode 100644 libs/esptk/.gitignore create mode 100644 libs/esptk/.pre-commit-config.yaml create mode 100644 libs/esptk/CMakeLists.txt create mode 100644 libs/esptk/CMakePresets.json create mode 100644 libs/esptk/README.md create mode 100644 libs/esptk/Version.cmake create mode 100644 libs/esptk/cmake/config.cmake.in create mode 100644 libs/esptk/include/esptk/espexceptions.h create mode 100644 libs/esptk/include/esptk/espfile.h create mode 100644 libs/esptk/include/esptk/esptypes.h create mode 100644 libs/esptk/include/esptk/record.h create mode 100644 libs/esptk/include/esptk/subrecord.h create mode 100644 libs/esptk/include/esptk/tes3record.h create mode 100644 libs/esptk/include/esptk/tes3subrecord.h create mode 100644 libs/esptk/src/CMakeLists.txt create mode 100644 libs/esptk/src/espfile.cpp create mode 100644 libs/esptk/src/record.cpp create mode 100644 libs/esptk/src/subrecord.cpp create mode 100644 libs/esptk/src/tes3record.cpp create mode 100644 libs/esptk/src/tes3subrecord.cpp create mode 100644 libs/fnistool/.editorconfig create mode 100644 libs/fnistool/.gitignore create mode 100644 libs/fnistool/.pre-commit-config.yaml create mode 100644 libs/fnistool/CMakeLists.txt create mode 100644 libs/fnistool/CMakePresets.json create mode 100644 libs/fnistool/LICENSE create mode 100644 libs/fnistool/src/CMakeLists.txt create mode 100644 libs/fnistool/src/FNISPatches.py create mode 100644 libs/fnistool/src/FNISTool.py create mode 100644 libs/fnistool/src/FNISToolReset.py create mode 100644 libs/fnistool/src/FNISTool_en.ts create mode 100644 libs/fnistool/vcpkg.json create mode 100644 libs/form43_checker/.editorconfig create mode 100644 libs/form43_checker/.gitignore create mode 100644 libs/form43_checker/.pre-commit-config.yaml create mode 100644 libs/form43_checker/CMakeLists.txt create mode 100644 libs/form43_checker/CMakePresets.json create mode 100644 libs/form43_checker/LICENSE create mode 100644 libs/form43_checker/src/CMakeLists.txt create mode 100644 libs/form43_checker/src/Form43Checker.py create mode 100644 libs/form43_checker/src/Form43Checker_en.ts create mode 100644 libs/form43_checker/vcpkg.json create mode 100644 libs/game_bethesda/.clang-format create mode 100644 libs/game_bethesda/.git-blame-ignore-revs create mode 100644 libs/game_bethesda/.gitattributes create mode 100644 libs/game_bethesda/.github/workflows/build.yml create mode 100644 libs/game_bethesda/.github/workflows/linting.yml create mode 100644 libs/game_bethesda/.gitignore create mode 100644 libs/game_bethesda/.pre-commit-config.yaml create mode 100644 libs/game_bethesda/CMakeLists.txt create mode 100644 libs/game_bethesda/CMakePresets.json create mode 100644 libs/game_bethesda/src/creation/CMakeLists.txt create mode 100644 libs/game_bethesda/src/creation/creationgameplugins.cpp create mode 100644 libs/game_bethesda/src/creation/creationgameplugins.h create mode 100644 libs/game_bethesda/src/creation/game_creation_en.ts create mode 100644 libs/game_bethesda/src/gamebryo/CMakeLists.txt create mode 100644 libs/game_bethesda/src/gamebryo/dummybsa.cpp create mode 100644 libs/game_bethesda/src/gamebryo/dummybsa.h create mode 100644 libs/game_bethesda/src/gamebryo/game_gamebryo_en.ts create mode 100644 libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.h create mode 100644 libs/game_bethesda/src/gamebryo/gamebryodataarchives.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryodataarchives.h create mode 100644 libs/game_bethesda/src/gamebryo/gamebryogameplugins.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryogameplugins.h create mode 100644 libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.h create mode 100644 libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.h create mode 100644 libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.h create mode 100644 libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryosavegame.h create mode 100644 libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.h create mode 100644 libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.h create mode 100644 libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.ui create mode 100644 libs/game_bethesda/src/gamebryo/gamebryoscriptextender.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryoscriptextender.h create mode 100644 libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.h create mode 100644 libs/game_bethesda/src/gamebryo/gamegamebryo.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamegamebryo.h create mode 100644 libs/game_bethesda/src/gamebryo/vdf_parser.h create mode 100644 libs/game_bethesda/src/games/enderal/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderaldataarchives.h create mode 100644 libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderalgameplugins.h create mode 100644 libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderallocalsavegames.h create mode 100644 libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/enderal/enderalsavegame.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderalsavegame.h create mode 100644 libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderalscriptextender.h create mode 100644 libs/game_bethesda/src/games/enderal/game_enderal_en.ts create mode 100644 libs/game_bethesda/src/games/enderal/gameenderal.cpp create mode 100644 libs/game_bethesda/src/games/enderal/gameenderal.h create mode 100644 libs/game_bethesda/src/games/enderal/gameenderal.json create mode 100644 libs/game_bethesda/src/games/enderalse/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsedataarchives.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsesavegame.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h create mode 100644 libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h create mode 100644 libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts create mode 100644 libs/game_bethesda/src/games/enderalse/gameenderalse.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/gameenderalse.h create mode 100644 libs/game_bethesda/src/games/enderalse/gameenderalse.json create mode 100644 libs/game_bethesda/src/games/fallout3/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.h create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3dataarchives.h create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3moddatachecker.h create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3moddatacontent.h create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3savegame.h create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3scriptextender.h create mode 100644 libs/game_bethesda/src/games/fallout3/game_fallout3_en.ts create mode 100644 libs/game_bethesda/src/games/fallout3/gamefallout3.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/gamefallout3.h create mode 100644 libs/game_bethesda/src/games/fallout3/gamefallout3.json create mode 100644 libs/game_bethesda/src/games/fallout4/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4.qrc create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.h create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4dataarchives.h create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4moddatachecker.h create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4moddatacontent.h create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4savegame.h create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4scriptextender.h create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.h create mode 100644 libs/game_bethesda/src/games/fallout4/game_fallout4_en.ts create mode 100644 libs/game_bethesda/src/games/fallout4/gamefallout4.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/gamefallout4.h create mode 100644 libs/game_bethesda/src/games/fallout4/gamefallout4.json create mode 100644 libs/game_bethesda/src/games/fallout4/splash.png create mode 100644 libs/game_bethesda/src/games/fallout4london/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4london.qrc create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.h create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.h create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.h create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.h create mode 100644 libs/game_bethesda/src/games/fallout4london/game_fo4london_en.ts create mode 100644 libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/gamefo4london.h create mode 100644 libs/game_bethesda/src/games/fallout4london/gamefo4london.json create mode 100644 libs/game_bethesda/src/games/fallout4london/splash.png create mode 100644 libs/game_bethesda/src/games/fallout4vr/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vr.qrc create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/game_fallout4vr_en.ts create mode 100644 libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.h create mode 100644 libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.json create mode 100644 libs/game_bethesda/src/games/fallout4vr/splash.png create mode 100644 libs/game_bethesda/src/games/fallout76/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76.qrc create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76dataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76dataarchives.h create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76moddatachecker.h create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76moddatacontent.h create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76savegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76savegame.h create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.h create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76scriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76scriptextender.h create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.h create mode 100644 libs/game_bethesda/src/games/fallout76/game_fallout76_en.ts create mode 100644 libs/game_bethesda/src/games/fallout76/gamefallout76.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/gamefallout76.h create mode 100644 libs/game_bethesda/src/games/fallout76/gamefallout76.json create mode 100644 libs/game_bethesda/src/games/fallout76/splash.png create mode 100644 libs/game_bethesda/src/games/falloutnv/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.h create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.h create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.h create mode 100644 libs/game_bethesda/src/games/falloutnv/game_falloutNV_en.ts create mode 100644 libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/gamefalloutnv.h create mode 100644 libs/game_bethesda/src/games/falloutnv/gamefalloutnv.json create mode 100644 libs/game_bethesda/src/games/morrowind/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/morrowind/game_morrowind_en.ts create mode 100644 libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/gamemorrowind.h create mode 100644 libs/game_bethesda/src/games/morrowind/gamemorrowind.json create mode 100644 libs/game_bethesda/src/games/morrowind/morrowind.qrc create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowinddataarchives.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindgameplugins.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegame.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegame.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.h create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.ui create mode 100644 libs/game_bethesda/src/games/morrowind/splash.png create mode 100644 libs/game_bethesda/src/games/nehrim/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/nehrim/game_nehrim_en.ts create mode 100644 libs/game_bethesda/src/games/nehrim/gamenehrim.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/gamenehrim.h create mode 100644 libs/game_bethesda/src/games/nehrim/gamenehrim.json create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimdataarchives.h create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimsavegame.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimsavegame.h create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimscriptextender.h create mode 100644 libs/game_bethesda/src/games/oblivion/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/oblivion/game_oblivion_en.ts create mode 100644 libs/game_bethesda/src/games/oblivion/gameoblivion.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/gameoblivion.h create mode 100644 libs/game_bethesda/src/games/oblivion/gameoblivion.json create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/obliviondataarchives.h create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionsavegame.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionsavegame.h create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionscriptextender.h create mode 100644 libs/game_bethesda/src/games/skyrim/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/skyrim/game_skyrim_en.ts create mode 100644 libs/game_bethesda/src/games/skyrim/gameskyrim.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/gameskyrim.h create mode 100644 libs/game_bethesda/src/games/skyrim/gameskyrim.json create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimdataarchives.h create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimgameplugins.h create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimsavegame.h create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimscriptextender.h create mode 100644 libs/game_bethesda/src/games/skyrimse/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/skyrimse/game_skyrimse_en.ts create mode 100644 libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/gameskyrimse.h create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.h create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsemoddatachecker.h create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsemoddatacontent.h create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.h create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.h create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/skyrimvr/game_skyrimvr_en.ts create mode 100644 libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.json create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.h create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.h create mode 100644 libs/game_bethesda/src/games/starfield/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/starfield/game_starfield_en.ts create mode 100644 libs/game_bethesda/src/games/starfield/gamestarfield.cpp create mode 100644 libs/game_bethesda/src/games/starfield/gamestarfield.h create mode 100644 libs/game_bethesda/src/games/starfield/gamestarfield.json create mode 100644 libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfielddataarchives.h create mode 100644 libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldgameplugins.h create mode 100644 libs/game_bethesda/src/games/starfield/starfieldmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/starfield/starfieldmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldsavegame.h create mode 100644 libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldscriptextender.h create mode 100644 libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h create mode 100644 libs/game_bethesda/src/games/ttw/CMakeLists.txt create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.h create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwdataarchives.h create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwmoddatachecker.h create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwmoddatacontent.h create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwsavegame.h create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwscriptextender.h create mode 100644 libs/game_bethesda/src/games/ttw/game_ttw_en.ts create mode 100644 libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp create mode 100644 libs/game_bethesda/src/games/ttw/gamefalloutttw.h create mode 100644 libs/game_bethesda/src/games/ttw/gamefalloutttw.json create mode 100644 libs/game_bethesda/vcpkg.json create mode 100644 libs/game_features/.gitignore create mode 100644 libs/game_features/CMakeLists.txt create mode 100644 libs/game_features/src/CMakeLists.txt create mode 100644 libs/game_features/src/bsainvalidation.h create mode 100644 libs/game_features/src/dataarchives.h create mode 100644 libs/game_features/src/gamefeatures.pro create mode 100644 libs/game_features/src/gamefeatures_en.ts create mode 100644 libs/game_features/src/gameplugins.h create mode 100644 libs/game_features/src/localsavegames.h create mode 100644 libs/game_features/src/moddatachecker.h create mode 100644 libs/game_features/src/moddatacontent.h create mode 100644 libs/game_features/src/savegameinfo.h create mode 100644 libs/game_features/src/scriptextender.h create mode 100644 libs/game_features/src/unmanagedmods.h create mode 100644 libs/game_gamebryo/.clang-format create mode 100644 libs/game_gamebryo/.git-blame-ignore-revs create mode 100644 libs/game_gamebryo/.gitattributes create mode 100644 libs/game_gamebryo/.github/workflows/build.yml create mode 100644 libs/game_gamebryo/.github/workflows/linting.yml create mode 100644 libs/game_gamebryo/.gitignore create mode 100644 libs/game_gamebryo/CMakeLists.txt create mode 100644 libs/game_gamebryo/README.md create mode 100644 libs/game_gamebryo/src/creation/CMakeLists.txt create mode 100644 libs/game_gamebryo/src/creation/creationgameplugins.cpp create mode 100644 libs/game_gamebryo/src/creation/creationgameplugins.h create mode 100644 libs/game_gamebryo/src/creation/game_creation_en.ts create mode 100644 libs/game_gamebryo/src/gamebryo/CMakeLists.txt create mode 100644 libs/game_gamebryo/src/gamebryo/dummybsa.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/dummybsa.h create mode 100644 libs/game_gamebryo/src/gamebryo/game_gamebryo_en.ts create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.h create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryodataarchives.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryodataarchives.h create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryogameplugins.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryogameplugins.h create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.h create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.h create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.h create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryosavegame.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryosavegame.h create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.h create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.h create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.ui create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.h create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.h create mode 100644 libs/game_gamebryo/src/gamebryo/gamegamebryo.cpp create mode 100644 libs/game_gamebryo/src/gamebryo/gamegamebryo.h create mode 100644 libs/game_gamebryo/src/gamebryo/vdf_parser.h create mode 100644 libs/installer_bain/.clang-format create mode 100644 libs/installer_bain/.git-blame-ignore-revs create mode 100644 libs/installer_bain/.gitattributes create mode 100644 libs/installer_bain/.github/workflows/build.yml create mode 100644 libs/installer_bain/.github/workflows/linting.yml create mode 100644 libs/installer_bain/.gitignore create mode 100644 libs/installer_bain/.pre-commit-config.yaml create mode 100644 libs/installer_bain/CMakeLists.txt create mode 100644 libs/installer_bain/CMakePresets.json create mode 100644 libs/installer_bain/src/CMakeLists.txt create mode 100644 libs/installer_bain/src/baincomplexinstallerdialog.cpp create mode 100644 libs/installer_bain/src/baincomplexinstallerdialog.h create mode 100644 libs/installer_bain/src/baincomplexinstallerdialog.ui create mode 100644 libs/installer_bain/src/installer_bain_en.ts create mode 100644 libs/installer_bain/src/installerbain.cpp create mode 100644 libs/installer_bain/src/installerbain.h create mode 100644 libs/installer_bain/vcpkg.json create mode 100644 libs/installer_bundle/.clang-format create mode 100644 libs/installer_bundle/.git-blame-ignore-revs create mode 100644 libs/installer_bundle/.gitattributes create mode 100644 libs/installer_bundle/.github/workflows/build.yml create mode 100644 libs/installer_bundle/.github/workflows/linting.yml create mode 100644 libs/installer_bundle/.gitignore create mode 100644 libs/installer_bundle/.pre-commit-config.yaml create mode 100644 libs/installer_bundle/CMakeLists.txt create mode 100644 libs/installer_bundle/CMakePresets.json create mode 100644 libs/installer_bundle/src/CMakeLists.txt create mode 100644 libs/installer_bundle/src/installer_bundle_en.ts create mode 100644 libs/installer_bundle/src/installerbundle.cpp create mode 100644 libs/installer_bundle/src/installerbundle.h create mode 100644 libs/installer_bundle/src/multiarchivedialog.cpp create mode 100644 libs/installer_bundle/src/multiarchivedialog.h create mode 100644 libs/installer_bundle/src/multiarchivedialog.ui create mode 100644 libs/installer_bundle/vcpkg.json create mode 100644 libs/installer_fomod/.clang-format create mode 100644 libs/installer_fomod/.git-blame-ignore-revs create mode 100644 libs/installer_fomod/.gitattributes create mode 100644 libs/installer_fomod/.github/workflows/build.yml create mode 100644 libs/installer_fomod/.github/workflows/linting.yml create mode 100644 libs/installer_fomod/.gitignore create mode 100644 libs/installer_fomod/.pre-commit-config.yaml create mode 100644 libs/installer_fomod/CMakeLists.txt create mode 100644 libs/installer_fomod/CMakePresets.json create mode 100644 libs/installer_fomod/src/CMakeLists.txt create mode 100644 libs/installer_fomod/src/fomodinstallerdialog.cpp create mode 100644 libs/installer_fomod/src/fomodinstallerdialog.h create mode 100644 libs/installer_fomod/src/fomodinstallerdialog.ui create mode 100644 libs/installer_fomod/src/fomodscreenshotdialog.cpp create mode 100644 libs/installer_fomod/src/fomodscreenshotdialog.h create mode 100644 libs/installer_fomod/src/fomodscreenshotdialog.ui create mode 100644 libs/installer_fomod/src/installer_fomod_en.ts create mode 100644 libs/installer_fomod/src/installerfomod.cpp create mode 100644 libs/installer_fomod/src/installerfomod.h create mode 100644 libs/installer_fomod/src/resources.qrc create mode 100644 libs/installer_fomod/src/resources/CloseButtonIcon.png create mode 100644 libs/installer_fomod/src/resources/LeftButtonIcon.png create mode 100644 libs/installer_fomod/src/resources/RightButtonIcon.png create mode 100644 libs/installer_fomod/src/scalelabel.cpp create mode 100644 libs/installer_fomod/src/scalelabel.h create mode 100644 libs/installer_fomod/src/xmlreader.cpp create mode 100644 libs/installer_fomod/src/xmlreader.h create mode 100644 libs/installer_fomod/vcpkg.json create mode 100644 libs/installer_fomod_csharp/.clang-format create mode 100644 libs/installer_fomod_csharp/.git-blame-ignore-revs create mode 100644 libs/installer_fomod_csharp/.gitattributes create mode 100644 libs/installer_fomod_csharp/.github/workflows/build.yml create mode 100644 libs/installer_fomod_csharp/.github/workflows/linting.yml create mode 100644 libs/installer_fomod_csharp/.gitignore create mode 100644 libs/installer_fomod_csharp/.pre-commit-config.yaml create mode 100644 libs/installer_fomod_csharp/CMakeLists.txt create mode 100644 libs/installer_fomod_csharp/CMakePresets.json create mode 100644 libs/installer_fomod_csharp/LICENSE create mode 100644 libs/installer_fomod_csharp/README.md create mode 100644 libs/installer_fomod_csharp/src/CMakeLists.txt create mode 100644 libs/installer_fomod_csharp/src/base_script.cpp create mode 100644 libs/installer_fomod_csharp/src/base_script.h create mode 100644 libs/installer_fomod_csharp/src/csharp_interface.cpp create mode 100644 libs/installer_fomod_csharp/src/csharp_interface.h create mode 100644 libs/installer_fomod_csharp/src/csharp_utils.h create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_csharp.cpp create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_csharp.h create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_csharp_en.ts create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_csharp_postdialog.ui create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_csharp_predialog.ui create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_postdialog.h create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_predialog.h create mode 100644 libs/installer_fomod_csharp/src/psettings.h create mode 100644 libs/installer_fomod_csharp/src/xml_info_reader.h create mode 100644 libs/installer_fomod_csharp/vcpkg.json create mode 100644 libs/installer_manual/.clang-format create mode 100644 libs/installer_manual/.git-blame-ignore-revs create mode 100644 libs/installer_manual/.gitattributes create mode 100644 libs/installer_manual/.github/workflows/build.yml create mode 100644 libs/installer_manual/.github/workflows/linting.yml create mode 100644 libs/installer_manual/.gitignore create mode 100644 libs/installer_manual/.pre-commit-config.yaml create mode 100644 libs/installer_manual/CMakeLists.txt create mode 100644 libs/installer_manual/CMakePresets.json create mode 100644 libs/installer_manual/src/CMakeLists.txt create mode 100644 libs/installer_manual/src/archivetree.cpp create mode 100644 libs/installer_manual/src/archivetree.h create mode 100644 libs/installer_manual/src/installdialog.cpp create mode 100644 libs/installer_manual/src/installdialog.h create mode 100644 libs/installer_manual/src/installdialog.ui create mode 100644 libs/installer_manual/src/installer_manual_en.ts create mode 100644 libs/installer_manual/src/installermanual.cpp create mode 100644 libs/installer_manual/src/installermanual.h create mode 100644 libs/installer_manual/vcpkg.json create mode 100644 libs/installer_omod/.editorconfig create mode 100644 libs/installer_omod/.gitattributes create mode 100644 libs/installer_omod/.github/workflows/build.yml create mode 100644 libs/installer_omod/.gitignore create mode 100644 libs/installer_omod/.pre-commit-config.yaml create mode 100644 libs/installer_omod/CMakeLists.txt create mode 100644 libs/installer_omod/CMakePresets.json create mode 100644 libs/installer_omod/LICENSE create mode 100644 libs/installer_omod/src/CMakeLists.txt create mode 100644 libs/installer_omod/src/DummyCSFile.cs create mode 100644 libs/installer_omod/src/MessageBoxHelper.cpp create mode 100644 libs/installer_omod/src/MessageBoxHelper.h create mode 100644 libs/installer_omod/src/OMODFrameworkWrapper.cpp create mode 100644 libs/installer_omod/src/OMODFrameworkWrapper.h create mode 100644 libs/installer_omod/src/QObject_unique_ptr.h create mode 100644 libs/installer_omod/src/implementations/CodeProgress.cpp create mode 100644 libs/installer_omod/src/implementations/CodeProgress.h create mode 100644 libs/installer_omod/src/implementations/Logger.cpp create mode 100644 libs/installer_omod/src/implementations/Logger.h create mode 100644 libs/installer_omod/src/implementations/ScriptFunctions.cpp create mode 100644 libs/installer_omod/src/implementations/ScriptFunctions.h create mode 100644 libs/installer_omod/src/installerOmod.cpp create mode 100644 libs/installer_omod/src/installerOmod.h create mode 100644 libs/installer_omod/src/installer_omod_en.ts create mode 100644 libs/installer_omod/src/interop/QtDotNetConverters.cpp create mode 100644 libs/installer_omod/src/interop/QtDotNetConverters.h create mode 100644 libs/installer_omod/src/interop/StdDotNetConverters.cpp create mode 100644 libs/installer_omod/src/interop/StdDotNetConverters.h create mode 100644 libs/installer_omod/src/newstuff/namedialog.cpp create mode 100644 libs/installer_omod/src/newstuff/namedialog.h create mode 100644 libs/installer_omod/src/newstuff/namedialog.ui create mode 100644 libs/installer_omod/src/newstuff/rtfPopup.cpp create mode 100644 libs/installer_omod/src/newstuff/rtfPopup.h create mode 100644 libs/installer_omod/src/oldstuff/DialogSelect.cpp create mode 100644 libs/installer_omod/src/oldstuff/DialogSelect.h create mode 100644 libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE create mode 100644 libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp create mode 100644 libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.h create mode 100644 libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.cpp create mode 100644 libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.h create mode 100644 libs/installer_omod/src/oldstuff/folder.md create mode 100644 libs/installer_omod/vcpkg.json create mode 100644 libs/installer_quick/.clang-format create mode 100644 libs/installer_quick/.git-blame-ignore-revs create mode 100644 libs/installer_quick/.gitattributes create mode 100644 libs/installer_quick/.github/workflows/build.yml create mode 100644 libs/installer_quick/.github/workflows/linting.yml create mode 100644 libs/installer_quick/.gitignore create mode 100644 libs/installer_quick/.pre-commit-config.yaml create mode 100644 libs/installer_quick/CMakeLists.txt create mode 100644 libs/installer_quick/CMakePresets.json create mode 100644 libs/installer_quick/src/CMakeLists.txt create mode 100644 libs/installer_quick/src/installer_quick_en.ts create mode 100644 libs/installer_quick/src/installerquick.cpp create mode 100644 libs/installer_quick/src/installerquick.h create mode 100644 libs/installer_quick/src/simpleinstalldialog.cpp create mode 100644 libs/installer_quick/src/simpleinstalldialog.h create mode 100644 libs/installer_quick/src/simpleinstalldialog.ui create mode 100644 libs/installer_quick/vcpkg.json create mode 100644 libs/installer_wizard/.gitattributes create mode 100644 libs/installer_wizard/.github/workflows/build.yml create mode 100644 libs/installer_wizard/.github/workflows/linters.yml create mode 100644 libs/installer_wizard/.gitignore create mode 100644 libs/installer_wizard/.pre-commit-config.yaml create mode 100644 libs/installer_wizard/CMakeLists.txt create mode 100644 libs/installer_wizard/CMakePresets.json create mode 100644 libs/installer_wizard/LICENSE create mode 100644 libs/installer_wizard/README.md create mode 100644 libs/installer_wizard/make-release.ps1 create mode 100644 libs/installer_wizard/make.ps1 create mode 100644 libs/installer_wizard/plugin-requirements.txt create mode 100644 libs/installer_wizard/poetry.lock create mode 100644 libs/installer_wizard/pyproject.toml create mode 100644 libs/installer_wizard/src/CMakeLists.txt create mode 100644 libs/installer_wizard/src/__init__.py create mode 100644 libs/installer_wizard/src/dialog.py create mode 100644 libs/installer_wizard/src/installer.py create mode 100644 libs/installer_wizard/src/installer_wizard_en.ts create mode 100644 libs/installer_wizard/src/runner.py create mode 100644 libs/installer_wizard/src/ui/wizardinstallercomplete.ui create mode 100644 libs/installer_wizard/src/ui/wizardinstallerdialog.ui create mode 100644 libs/installer_wizard/src/ui/wizardinstallererror.ui create mode 100644 libs/installer_wizard/src/ui/wizardinstallerpage.ui create mode 100644 libs/installer_wizard/src/ui/wizardinstallerrequires.ui create mode 100644 libs/installer_wizard/src/utils.py create mode 100644 libs/installer_wizard/vcpkg.json create mode 100644 libs/libbsarch/.clang-format create mode 100644 libs/libbsarch/.gitattributes create mode 100644 libs/libbsarch/.github/workflows/build.yml create mode 100644 libs/libbsarch/.github/workflows/linting.yml create mode 100644 libs/libbsarch/.gitignore create mode 100644 libs/libbsarch/.pre-commit-config.yaml create mode 100644 libs/libbsarch/CMakeLists.txt create mode 100644 libs/libbsarch/CMakePresets.json create mode 100644 libs/libbsarch/LICENSE create mode 100644 libs/libbsarch/README.md create mode 100644 libs/libbsarch/Version.cmake create mode 100644 libs/libbsarch/cmake/config.cmake.in create mode 100644 libs/libbsarch/delphi/libbsarch.dpr create mode 100644 libs/libbsarch/delphi/libbsarch.dproj create mode 100644 libs/libbsarch/delphi/wbBSArchive.pas create mode 100644 libs/libbsarch/examples/libbsarch-visualstudio-test.cpp create mode 100644 libs/libbsarch/examples/libbsarch-visualstudio-test.sln create mode 100644 libs/libbsarch/examples/test_read.ba2 create mode 100644 libs/libbsarch/examples/test_read.bsa create mode 100644 libs/libbsarch/include/libbsarch/base_types.hpp create mode 100644 libs/libbsarch/include/libbsarch/bs_archive.h create mode 100644 libs/libbsarch/include/libbsarch/bs_archive_auto.hpp create mode 100644 libs/libbsarch/include/libbsarch/bs_archive_entries.h create mode 100644 libs/libbsarch/include/libbsarch/libbsarch.h create mode 100644 libs/libbsarch/include/libbsarch/libbsarch.hpp create mode 100644 libs/libbsarch/include/libbsarch/utils/convertible_ostream.hpp create mode 100644 libs/libbsarch/include/libbsarch/utils/convertible_string.hpp create mode 100644 libs/libbsarch/include/libbsarch/utils/string_convert.hpp create mode 100644 libs/libbsarch/src/bs_archive.cpp create mode 100644 libs/libbsarch/src/bs_archive_auto.cpp create mode 100644 libs/libbsarch/src/bs_archive_entries.cpp create mode 100644 libs/libbsarch/src/libbsarch.cpp create mode 100644 libs/libbsarch/src/libbsarch.def create mode 100644 libs/libbsarch/src/utils/convertible_ostream.cpp create mode 100644 libs/libbsarch/src/utils/convertible_string.cpp create mode 100644 libs/libbsarch/src/utils/string_convert.cpp create mode 100644 libs/libbsarch/vcpkg.json create mode 100644 libs/lootcli/.clang-format create mode 100644 libs/lootcli/.git-blame-ignore-revs create mode 100644 libs/lootcli/.gitattributes create mode 100644 libs/lootcli/.github/workflows/build.yml create mode 100644 libs/lootcli/.github/workflows/linting.yml create mode 100644 libs/lootcli/.gitignore create mode 100644 libs/lootcli/.pre-commit-config.yaml create mode 100644 libs/lootcli/CMakeLists.txt create mode 100644 libs/lootcli/CMakePresets.json create mode 100644 libs/lootcli/README.md create mode 100644 libs/lootcli/cmake/config.cmake.in create mode 100644 libs/lootcli/include/lootcli/lootcli.h create mode 100644 libs/lootcli/src/CMakeLists.txt create mode 100644 libs/lootcli/src/game_settings.cpp create mode 100644 libs/lootcli/src/game_settings.h create mode 100644 libs/lootcli/src/lootthread.cpp create mode 100644 libs/lootcli/src/lootthread.h create mode 100644 libs/lootcli/src/main.cpp create mode 100644 libs/lootcli/src/pch.cpp create mode 100644 libs/lootcli/src/pch.h create mode 100644 libs/lootcli/src/version.h create mode 100644 libs/lootcli/src/version.rc create mode 100644 libs/lootcli/vcpkg.json create mode 100644 libs/lzokay.py create mode 100644 libs/nak_ffi/CMakeLists.txt create mode 100644 libs/nak_ffi/Cargo.lock create mode 100644 libs/nak_ffi/Cargo.toml create mode 100644 libs/nak_ffi/include/nak_ffi.h create mode 100644 libs/nak_ffi/src/lib.rs create mode 100644 libs/plugin_python/.clang-format create mode 100644 libs/plugin_python/.git-blame-ignore-revs create mode 100644 libs/plugin_python/.gitattributes create mode 100644 libs/plugin_python/.github/workflows/build-and-test.yml create mode 100644 libs/plugin_python/.github/workflows/linting.yml create mode 100644 libs/plugin_python/.gitignore create mode 100644 libs/plugin_python/.pre-commit-config.yaml create mode 100644 libs/plugin_python/CMakeLists.txt create mode 100644 libs/plugin_python/CMakePresets.json create mode 100644 libs/plugin_python/README.md create mode 100644 libs/plugin_python/poetry.lock create mode 100644 libs/plugin_python/pyproject.toml create mode 100644 libs/plugin_python/src/CMakeLists.txt create mode 100644 libs/plugin_python/src/mobase/CMakeLists.txt create mode 100644 libs/plugin_python/src/mobase/README.md create mode 100644 libs/plugin_python/src/mobase/deprecation.cpp create mode 100644 libs/plugin_python/src/mobase/deprecation.h create mode 100644 libs/plugin_python/src/mobase/mobase.cpp create mode 100644 libs/plugin_python/src/mobase/pybind11_all.h create mode 100644 libs/plugin_python/src/mobase/wrappers/basic_classes.cpp create mode 100644 libs/plugin_python/src/mobase/wrappers/game_features.cpp create mode 100644 libs/plugin_python/src/mobase/wrappers/known_folders.h create mode 100644 libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp create mode 100644 libs/plugin_python/src/mobase/wrappers/pyfiletree.h create mode 100644 libs/plugin_python/src/mobase/wrappers/pyplugins.cpp create mode 100644 libs/plugin_python/src/mobase/wrappers/pyplugins.h create mode 100644 libs/plugin_python/src/mobase/wrappers/utils.cpp create mode 100644 libs/plugin_python/src/mobase/wrappers/widgets.cpp create mode 100644 libs/plugin_python/src/mobase/wrappers/wrappers.cpp create mode 100644 libs/plugin_python/src/mobase/wrappers/wrappers.h create mode 100644 libs/plugin_python/src/plugin_python_en.ts create mode 100644 libs/plugin_python/src/proxy/CMakeLists.txt create mode 100644 libs/plugin_python/src/proxy/build_pythoncore.py create mode 100644 libs/plugin_python/src/proxy/proxypython.cpp create mode 100644 libs/plugin_python/src/proxy/proxypython.h create mode 100644 libs/plugin_python/src/pybind11-qt/CMakeLists.txt create mode 100644 libs/plugin_python/src/pybind11-qt/README.md create mode 100644 libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_enum.h create mode 100644 libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qlist.h create mode 100644 libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qmap.h create mode 100644 libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h create mode 100644 libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_utils.h create mode 100644 libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h create mode 100644 libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_basic.h create mode 100644 libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_containers.h create mode 100644 libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_enums.h create mode 100644 libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_holder.h create mode 100644 libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_objects.h create mode 100644 libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_qflags.h create mode 100644 libs/plugin_python/src/pybind11-qt/pybind11_qt_basic.cpp create mode 100644 libs/plugin_python/src/pybind11-qt/pybind11_qt_sip.cpp create mode 100644 libs/plugin_python/src/pybind11-qt/pybind11_qt_utils.cpp create mode 100644 libs/plugin_python/src/pybind11-utils/CMakeLists.txt create mode 100644 libs/plugin_python/src/pybind11-utils/README.md create mode 100644 libs/plugin_python/src/pybind11-utils/functional.cpp create mode 100644 libs/plugin_python/src/pybind11-utils/include/pybind11_utils/functional.h create mode 100644 libs/plugin_python/src/pybind11-utils/include/pybind11_utils/generator.h create mode 100644 libs/plugin_python/src/pybind11-utils/include/pybind11_utils/shared_cpp_owner.h create mode 100644 libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant.h create mode 100644 libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant_wrapper.h create mode 100644 libs/plugin_python/src/runner/CMakeLists.txt create mode 100644 libs/plugin_python/src/runner/error.h create mode 100644 libs/plugin_python/src/runner/pythonrunner.cpp create mode 100644 libs/plugin_python/src/runner/pythonrunner.h create mode 100644 libs/plugin_python/src/runner/pythonutils.cpp create mode 100644 libs/plugin_python/src/runner/pythonutils.h create mode 100644 libs/plugin_python/tests/CMakeLists.txt create mode 100644 libs/plugin_python/tests/mocks/DummyFileTree.h create mode 100644 libs/plugin_python/tests/mocks/MockOrganizer.h create mode 100644 libs/plugin_python/tests/python/CMakeLists.txt create mode 100644 libs/plugin_python/tests/python/conftest.py create mode 100644 libs/plugin_python/tests/python/test_argument_wrapper.cpp create mode 100644 libs/plugin_python/tests/python/test_argument_wrapper.py create mode 100644 libs/plugin_python/tests/python/test_filetree.cpp create mode 100644 libs/plugin_python/tests/python/test_filetree.py create mode 100644 libs/plugin_python/tests/python/test_functional.cpp create mode 100644 libs/plugin_python/tests/python/test_functional.py create mode 100644 libs/plugin_python/tests/python/test_guessed_string.cpp create mode 100644 libs/plugin_python/tests/python/test_guessed_string.py create mode 100644 libs/plugin_python/tests/python/test_organizer.cpp create mode 100644 libs/plugin_python/tests/python/test_organizer.py create mode 100644 libs/plugin_python/tests/python/test_path_wrappers.py create mode 100644 libs/plugin_python/tests/python/test_qt.cpp create mode 100644 libs/plugin_python/tests/python/test_qt.py create mode 100644 libs/plugin_python/tests/python/test_qt_widgets.cpp create mode 100644 libs/plugin_python/tests/python/test_qt_widgets.py create mode 100644 libs/plugin_python/tests/python/test_shared_cpp_owner.cpp create mode 100644 libs/plugin_python/tests/python/test_shared_cpp_owner.py create mode 100644 libs/plugin_python/tests/runner/CMakeLists.txt create mode 100644 libs/plugin_python/tests/runner/plugins/dummy-diagnose.py create mode 100644 libs/plugin_python/tests/runner/plugins/dummy-filemapper.py create mode 100644 libs/plugin_python/tests/runner/plugins/dummy-game.py create mode 100644 libs/plugin_python/tests/runner/plugins/dummy-installer.py create mode 100644 libs/plugin_python/tests/runner/plugins/dummy-iplugin.py create mode 100644 libs/plugin_python/tests/runner/test_diagnose.cpp create mode 100644 libs/plugin_python/tests/runner/test_filemapper.cpp create mode 100644 libs/plugin_python/tests/runner/test_game.cpp create mode 100644 libs/plugin_python/tests/runner/test_installer.cpp create mode 100644 libs/plugin_python/tests/runner/test_iplugin.cpp create mode 100644 libs/plugin_python/tests/runner/test_lifetime.cpp create mode 100644 libs/plugin_python/typings/generate.py create mode 100644 libs/plugin_python/typings/mobase_tests/argument_wrapper.pyi create mode 100644 libs/plugin_python/typings/mobase_tests/filetree.pyi create mode 100644 libs/plugin_python/typings/mobase_tests/functional.pyi create mode 100644 libs/plugin_python/typings/mobase_tests/guessed_string.pyi create mode 100644 libs/plugin_python/typings/mobase_tests/organizer.pyi create mode 100644 libs/plugin_python/typings/mobase_tests/qt.pyi create mode 100644 libs/plugin_python/typings/mobase_tests/qt_widgets.pyi create mode 100644 libs/plugin_python/typings/mobase_tests/shared_cpp_owner.pyi create mode 100644 libs/plugin_python/vcpkg.json create mode 100644 libs/preview_base/.clang-format create mode 100644 libs/preview_base/.git-blame-ignore-revs create mode 100644 libs/preview_base/.gitattributes create mode 100644 libs/preview_base/.github/workflows/build.yml create mode 100644 libs/preview_base/.github/workflows/linting.yml create mode 100644 libs/preview_base/.gitignore create mode 100644 libs/preview_base/.pre-commit-config.yaml create mode 100644 libs/preview_base/CMakeLists.txt create mode 100644 libs/preview_base/CMakePresets.json create mode 100644 libs/preview_base/src/CMakeLists.txt create mode 100644 libs/preview_base/src/preview_base_en.ts create mode 100644 libs/preview_base/src/previewbase.cpp create mode 100644 libs/preview_base/src/previewbase.h create mode 100644 libs/preview_base/vcpkg.json create mode 100644 libs/preview_bsa/.clang-format create mode 100644 libs/preview_bsa/.gitattributes create mode 100644 libs/preview_bsa/.github/workflows/build.yml create mode 100644 libs/preview_bsa/.github/workflows/linting.yml create mode 100644 libs/preview_bsa/.gitignore create mode 100644 libs/preview_bsa/.pre-commit-config.yaml create mode 100644 libs/preview_bsa/CMakeLists.txt create mode 100644 libs/preview_bsa/CMakePresets.json create mode 100644 libs/preview_bsa/src/CMakeLists.txt create mode 100644 libs/preview_bsa/src/preview_bsa_en.ts create mode 100644 libs/preview_bsa/src/previewbsa.cpp create mode 100644 libs/preview_bsa/src/previewbsa.h create mode 100644 libs/preview_bsa/src/previewbsa.json create mode 100644 libs/preview_bsa/src/simplefiletreeitem.cpp create mode 100644 libs/preview_bsa/src/simplefiletreeitem.h create mode 100644 libs/preview_bsa/src/simplefiletreemodel.cpp create mode 100644 libs/preview_bsa/src/simplefiletreemodel.h create mode 100644 libs/preview_bsa/vcpkg.json create mode 100644 libs/preview_dds/.editorconfig create mode 100644 libs/preview_dds/.gitignore create mode 100644 libs/preview_dds/.pre-commit-config.yaml create mode 100644 libs/preview_dds/CMakeLists.txt create mode 100644 libs/preview_dds/CMakePresets.json create mode 100644 libs/preview_dds/LICENSE create mode 100644 libs/preview_dds/README.md create mode 100644 libs/preview_dds/src/CMakeLists.txt create mode 100644 libs/preview_dds/src/DDS/DDSDefinitions.py create mode 100644 libs/preview_dds/src/DDS/DDSFile.py create mode 100644 libs/preview_dds/src/DDS/__init__.py create mode 100644 libs/preview_dds/src/DDS/glstuff.py create mode 100644 libs/preview_dds/src/DDSPreview.py create mode 100644 libs/preview_dds/src/DDSPreview_en.ts create mode 100644 libs/preview_dds/vcpkg.json create mode 100644 libs/script_extender_plugin_checker/.editorconfig create mode 100644 libs/script_extender_plugin_checker/.gitignore create mode 100644 libs/script_extender_plugin_checker/.pre-commit-config.yaml create mode 100644 libs/script_extender_plugin_checker/CMakeLists.txt create mode 100644 libs/script_extender_plugin_checker/CMakePresets.json create mode 100644 libs/script_extender_plugin_checker/LICENSE create mode 100644 libs/script_extender_plugin_checker/src/CMakeLists.txt create mode 100644 libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker.py create mode 100644 libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker_en.ts create mode 100644 libs/script_extender_plugin_checker/vcpkg.json create mode 100644 libs/tool_inibakery/.clang-format create mode 100644 libs/tool_inibakery/.gitattributes create mode 100644 libs/tool_inibakery/.gitignore create mode 100644 libs/tool_inibakery/.pre-commit-config.yaml create mode 100644 libs/tool_inibakery/CMakeLists.txt create mode 100644 libs/tool_inibakery/CMakePresets.json create mode 100644 libs/tool_inibakery/src/CMakeLists.txt create mode 100644 libs/tool_inibakery/src/inibakery.cpp create mode 100644 libs/tool_inibakery/src/inibakery.h create mode 100644 libs/tool_inibakery/src/inibakery_en.ts create mode 100644 libs/tool_inibakery/vcpkg.json create mode 100644 libs/tool_inieditor/.clang-format create mode 100644 libs/tool_inieditor/.gitattributes create mode 100644 libs/tool_inieditor/.gitignore create mode 100644 libs/tool_inieditor/.pre-commit-config.yaml create mode 100644 libs/tool_inieditor/CMakeLists.txt create mode 100644 libs/tool_inieditor/CMakePresets.json create mode 100644 libs/tool_inieditor/src/CMakeLists.txt create mode 100644 libs/tool_inieditor/src/inieditor.cpp create mode 100644 libs/tool_inieditor/src/inieditor.h create mode 100644 libs/tool_inieditor/src/inieditor.qrc create mode 100644 libs/tool_inieditor/src/inieditor_en.ts create mode 100644 libs/tool_inieditor/src/resources/document-properties.png create mode 100644 libs/tool_inieditor/vcpkg.json create mode 100644 libs/uibase/.clang-format create mode 100644 libs/uibase/.git-blame-ignore-revs create mode 100644 libs/uibase/.gitattributes create mode 100644 libs/uibase/.github/workflows/build.yml create mode 100644 libs/uibase/.github/workflows/linting.yml create mode 100644 libs/uibase/.gitignore create mode 100644 libs/uibase/.pre-commit-config.yaml create mode 100644 libs/uibase/CMakeLists.txt create mode 100644 libs/uibase/CMakePresets.json create mode 100644 libs/uibase/README.md create mode 100644 libs/uibase/cmake/config.cmake.in create mode 100644 libs/uibase/include/uibase/delayedfilewriter.h create mode 100644 libs/uibase/include/uibase/diagnosisreport.h create mode 100644 libs/uibase/include/uibase/dllimport.h create mode 100644 libs/uibase/include/uibase/errorcodes.h create mode 100644 libs/uibase/include/uibase/eventfilter.h create mode 100644 libs/uibase/include/uibase/exceptions.h create mode 100644 libs/uibase/include/uibase/executableinfo.h create mode 100644 libs/uibase/include/uibase/expanderwidget.h create mode 100644 libs/uibase/include/uibase/filemapping.h create mode 100644 libs/uibase/include/uibase/filesystemutilities.h create mode 100644 libs/uibase/include/uibase/filterwidget.h create mode 100644 libs/uibase/include/uibase/finddialog.h create mode 100644 libs/uibase/include/uibase/formatters.h create mode 100644 libs/uibase/include/uibase/formatters/enums.h create mode 100644 libs/uibase/include/uibase/formatters/qt.h create mode 100644 libs/uibase/include/uibase/formatters/strings.h create mode 100644 libs/uibase/include/uibase/game_features/bsainvalidation.h create mode 100644 libs/uibase/include/uibase/game_features/dataarchives.h create mode 100644 libs/uibase/include/uibase/game_features/game_feature.h create mode 100644 libs/uibase/include/uibase/game_features/gameplugins.h create mode 100644 libs/uibase/include/uibase/game_features/igamefeatures.h create mode 100644 libs/uibase/include/uibase/game_features/localsavegames.h create mode 100644 libs/uibase/include/uibase/game_features/moddatachecker.h create mode 100644 libs/uibase/include/uibase/game_features/moddatacontent.h create mode 100644 libs/uibase/include/uibase/game_features/savegameinfo.h create mode 100644 libs/uibase/include/uibase/game_features/scriptextender.h create mode 100644 libs/uibase/include/uibase/game_features/unmanagedmods.h create mode 100644 libs/uibase/include/uibase/guessedvalue.h create mode 100644 libs/uibase/include/uibase/idownloadmanager.h create mode 100644 libs/uibase/include/uibase/iexecutable.h create mode 100644 libs/uibase/include/uibase/iexecutableslist.h create mode 100644 libs/uibase/include/uibase/ifiletree.h create mode 100644 libs/uibase/include/uibase/ifiletree_utils.h create mode 100644 libs/uibase/include/uibase/iinstallationmanager.h create mode 100644 libs/uibase/include/uibase/iinstance.h create mode 100644 libs/uibase/include/uibase/iinstancemanager.h create mode 100644 libs/uibase/include/uibase/imodinterface.h create mode 100644 libs/uibase/include/uibase/imodlist.h create mode 100644 libs/uibase/include/uibase/imodrepositorybridge.h create mode 100644 libs/uibase/include/uibase/imoinfo.h create mode 100644 libs/uibase/include/uibase/iplugin.h create mode 100644 libs/uibase/include/uibase/iplugindiagnose.h create mode 100644 libs/uibase/include/uibase/ipluginfilemapper.h create mode 100644 libs/uibase/include/uibase/iplugingame.h create mode 100644 libs/uibase/include/uibase/iplugingamefeatures.h create mode 100644 libs/uibase/include/uibase/iplugininstaller.h create mode 100644 libs/uibase/include/uibase/iplugininstallercustom.h create mode 100644 libs/uibase/include/uibase/iplugininstallersimple.h create mode 100644 libs/uibase/include/uibase/ipluginlist.h create mode 100644 libs/uibase/include/uibase/ipluginmodpage.h create mode 100644 libs/uibase/include/uibase/ipluginpreview.h create mode 100644 libs/uibase/include/uibase/ipluginproxy.h create mode 100644 libs/uibase/include/uibase/iplugintool.h create mode 100644 libs/uibase/include/uibase/iprofile.h create mode 100644 libs/uibase/include/uibase/isavegame.h create mode 100644 libs/uibase/include/uibase/isavegameinfowidget.h create mode 100644 libs/uibase/include/uibase/json.h create mode 100644 libs/uibase/include/uibase/lineeditclear.h create mode 100644 libs/uibase/include/uibase/linklabel.h create mode 100644 libs/uibase/include/uibase/log.h create mode 100644 libs/uibase/include/uibase/memoizedlock.h create mode 100644 libs/uibase/include/uibase/moassert.h create mode 100644 libs/uibase/include/uibase/modrepositoryfileinfo.h create mode 100644 libs/uibase/include/uibase/nxmurl.h create mode 100644 libs/uibase/include/uibase/pluginrequirements.h create mode 100644 libs/uibase/include/uibase/pluginsetting.h create mode 100644 libs/uibase/include/uibase/questionboxmemory.h create mode 100644 libs/uibase/include/uibase/registry.h create mode 100644 libs/uibase/include/uibase/report.h create mode 100644 libs/uibase/include/uibase/safewritefile.h create mode 100644 libs/uibase/include/uibase/scopeguard.h create mode 100644 libs/uibase/include/uibase/sortabletreewidget.h create mode 100644 libs/uibase/include/uibase/steamutility.h create mode 100644 libs/uibase/include/uibase/strings.h create mode 100644 libs/uibase/include/uibase/taskprogressmanager.h create mode 100644 libs/uibase/include/uibase/textviewer.h create mode 100644 libs/uibase/include/uibase/tutorabledialog.h create mode 100644 libs/uibase/include/uibase/tutorialcontrol.h create mode 100644 libs/uibase/include/uibase/tutorialmanager.h create mode 100644 libs/uibase/include/uibase/utility.h create mode 100644 libs/uibase/include/uibase/versioninfo.h create mode 100644 libs/uibase/include/uibase/versioning.h create mode 100644 libs/uibase/include/uibase/widgetutility.h create mode 120000 libs/uibase/include/uibase/windows_compat.h create mode 100644 libs/uibase/src/CMakeLists.txt create mode 100644 libs/uibase/src/delayedfilewriter.cpp create mode 100644 libs/uibase/src/diagnosisreport.cpp create mode 100644 libs/uibase/src/errorcodes.cpp create mode 100644 libs/uibase/src/eventfilter.cpp create mode 100644 libs/uibase/src/executableinfo.cpp create mode 100644 libs/uibase/src/expanderwidget.cpp create mode 100644 libs/uibase/src/filesystemutilities.cpp create mode 100644 libs/uibase/src/filterwidget.cpp create mode 100644 libs/uibase/src/finddialog.cpp create mode 100644 libs/uibase/src/finddialog.ui create mode 100644 libs/uibase/src/guessedvalue.cpp create mode 100644 libs/uibase/src/ifiletree.cpp create mode 100644 libs/uibase/src/imodrepositorybridge.cpp create mode 100644 libs/uibase/src/imoinfo.cpp create mode 100644 libs/uibase/src/json.cpp create mode 100644 libs/uibase/src/lineeditclear.cpp create mode 100644 libs/uibase/src/linklabel.cpp create mode 100644 libs/uibase/src/log.cpp create mode 100644 libs/uibase/src/modrepositoryfileinfo.cpp create mode 100644 libs/uibase/src/nxmurl.cpp create mode 100644 libs/uibase/src/pch.cpp create mode 100644 libs/uibase/src/pch.h create mode 100644 libs/uibase/src/pluginrequirements.cpp create mode 100644 libs/uibase/src/questionboxmemory.cpp create mode 100644 libs/uibase/src/questionboxmemory.ui create mode 100644 libs/uibase/src/registry.cpp create mode 100644 libs/uibase/src/report.cpp create mode 100644 libs/uibase/src/safewritefile.cpp create mode 100644 libs/uibase/src/scopeguard.cpp create mode 100644 libs/uibase/src/sortabletreewidget.cpp create mode 100644 libs/uibase/src/steamutility.cpp create mode 100644 libs/uibase/src/strings.cpp create mode 100644 libs/uibase/src/taskdialog.ui create mode 100644 libs/uibase/src/taskprogressmanager.cpp create mode 100644 libs/uibase/src/textviewer.cpp create mode 100644 libs/uibase/src/textviewer.ui create mode 100644 libs/uibase/src/tutorabledialog.cpp create mode 100644 libs/uibase/src/tutorialcontrol.cpp create mode 100644 libs/uibase/src/tutorialmanager.cpp create mode 100644 libs/uibase/src/uibase_en.ts create mode 100644 libs/uibase/src/utility.cpp create mode 100644 libs/uibase/src/version.rc create mode 100644 libs/uibase/src/versioninfo.cpp create mode 100644 libs/uibase/src/versioning.cpp create mode 100644 libs/uibase/src/widgetutility.cpp create mode 100644 libs/uibase/tests/CMakeLists.txt create mode 100644 libs/uibase/tests/cmake/CMakeLists.txt create mode 100644 libs/uibase/tests/cmake/plugin.cpp create mode 100644 libs/uibase/tests/test_formatters.cpp create mode 100644 libs/uibase/tests/test_ifiletree.cpp create mode 100644 libs/uibase/tests/test_main.cpp create mode 100644 libs/uibase/tests/test_strings.cpp create mode 100644 libs/uibase/tests/test_versioning.cpp create mode 100644 libs/uibase/tests/translations/tests_en.qm create mode 100644 libs/uibase/tests/translations/tests_en.ts create mode 100644 libs/uibase/tests/translations/tests_fr.qm create mode 100644 libs/uibase/tests/translations/tests_fr.ts create mode 100644 libs/uibase/vcpkg.json create mode 100644 libs/usvfs/.clang-format create mode 100644 libs/usvfs/.git-blame-ignore-revs create mode 100644 libs/usvfs/.gitattributes create mode 100644 libs/usvfs/.github/workflows/build.yml create mode 100644 libs/usvfs/.github/workflows/linting.yml create mode 100644 libs/usvfs/.gitignore create mode 100644 libs/usvfs/.pre-commit-config.yaml create mode 100644 libs/usvfs/CMakeLists.txt create mode 100644 libs/usvfs/CMakePresets.json create mode 100644 libs/usvfs/LICENSE create mode 100644 libs/usvfs/README.md create mode 100644 libs/usvfs/cmake/config.cmake.in create mode 100644 libs/usvfs/include/usvfs/dllimport.h create mode 100644 libs/usvfs/include/usvfs/logging.h create mode 100644 libs/usvfs/include/usvfs/sharedparameters.h create mode 100644 libs/usvfs/include/usvfs/usvfs.h create mode 100644 libs/usvfs/include/usvfs/usvfs_version.h create mode 100644 libs/usvfs/include/usvfs/usvfsparameters.h create mode 100644 libs/usvfs/include/usvfs/usvfsparametersprivate.h create mode 100644 libs/usvfs/licenses/asmjit.txt create mode 100644 libs/usvfs/licenses/boost.txt create mode 100644 libs/usvfs/licenses/cppformat.txt create mode 100644 libs/usvfs/licenses/googletest.txt create mode 100644 libs/usvfs/licenses/qt.txt create mode 100644 libs/usvfs/licenses/spdlog.txt create mode 100644 libs/usvfs/licenses/udis86.txt create mode 100644 libs/usvfs/src/shared/CMakeLists.txt create mode 100644 libs/usvfs/src/shared/addrtools.h create mode 100644 libs/usvfs/src/shared/directory_tree.cpp create mode 100644 libs/usvfs/src/shared/directory_tree.h create mode 100644 libs/usvfs/src/shared/exceptionex.cpp create mode 100644 libs/usvfs/src/shared/exceptionex.h create mode 100644 libs/usvfs/src/shared/formatters.h create mode 100644 libs/usvfs/src/shared/loghelpers.cpp create mode 100644 libs/usvfs/src/shared/loghelpers.h create mode 100644 libs/usvfs/src/shared/ntdll_declarations.cpp create mode 100644 libs/usvfs/src/shared/ntdll_declarations.h create mode 100644 libs/usvfs/src/shared/pch.cpp create mode 100644 libs/usvfs/src/shared/pch.h create mode 100644 libs/usvfs/src/shared/shared_memory.h create mode 100644 libs/usvfs/src/shared/shmlogger.cpp create mode 100644 libs/usvfs/src/shared/shmlogger.h create mode 100644 libs/usvfs/src/shared/stringcast.cpp create mode 100644 libs/usvfs/src/shared/stringcast.h create mode 100644 libs/usvfs/src/shared/stringutils.cpp create mode 100644 libs/usvfs/src/shared/stringutils.h create mode 100644 libs/usvfs/src/shared/tree_container.h create mode 100644 libs/usvfs/src/shared/unicodestring.cpp create mode 100644 libs/usvfs/src/shared/unicodestring.h create mode 100644 libs/usvfs/src/shared/wildcard.cpp create mode 100644 libs/usvfs/src/shared/wildcard.h create mode 100644 libs/usvfs/src/shared/winapi.cpp create mode 100644 libs/usvfs/src/shared/winapi.h create mode 100644 libs/usvfs/src/shared/windows_sane.h create mode 100644 libs/usvfs/src/thooklib/CMakeLists.txt create mode 100644 libs/usvfs/src/thooklib/asmjit_sane.h create mode 100644 libs/usvfs/src/thooklib/hooklib.cpp create mode 100644 libs/usvfs/src/thooklib/hooklib.h create mode 100644 libs/usvfs/src/thooklib/pch.cpp create mode 100644 libs/usvfs/src/thooklib/ttrampolinepool.cpp create mode 100644 libs/usvfs/src/thooklib/ttrampolinepool.h create mode 100644 libs/usvfs/src/thooklib/udis86wrapper.cpp create mode 100644 libs/usvfs/src/thooklib/udis86wrapper.h create mode 100644 libs/usvfs/src/thooklib/utility.cpp create mode 100644 libs/usvfs/src/thooklib/utility.h create mode 100644 libs/usvfs/src/tinjectlib/CMakeLists.txt create mode 100644 libs/usvfs/src/tinjectlib/asmjit_sane.h create mode 100644 libs/usvfs/src/tinjectlib/injectlib.cpp create mode 100644 libs/usvfs/src/tinjectlib/injectlib.h create mode 100644 libs/usvfs/src/usvfs_dll/CMakeLists.txt create mode 100644 libs/usvfs/src/usvfs_dll/hookcallcontext.cpp create mode 100644 libs/usvfs/src/usvfs_dll/hookcallcontext.h create mode 100644 libs/usvfs/src/usvfs_dll/hookcontext.cpp create mode 100644 libs/usvfs/src/usvfs_dll/hookcontext.h create mode 100644 libs/usvfs/src/usvfs_dll/hookmanager.cpp create mode 100644 libs/usvfs/src/usvfs_dll/hookmanager.h create mode 100644 libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h create mode 100644 libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp create mode 100644 libs/usvfs/src/usvfs_dll/hooks/kernel32.h create mode 100644 libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp create mode 100644 libs/usvfs/src/usvfs_dll/hooks/ntdll.h create mode 100644 libs/usvfs/src/usvfs_dll/hooks/sharedids.h create mode 100644 libs/usvfs/src/usvfs_dll/maptracker.h create mode 100644 libs/usvfs/src/usvfs_dll/pch.cpp create mode 100644 libs/usvfs/src/usvfs_dll/redirectiontree.cpp create mode 100644 libs/usvfs/src/usvfs_dll/redirectiontree.h create mode 100644 libs/usvfs/src/usvfs_dll/semaphore.cpp create mode 100644 libs/usvfs/src/usvfs_dll/semaphore.h create mode 100644 libs/usvfs/src/usvfs_dll/sharedparameters.cpp create mode 100644 libs/usvfs/src/usvfs_dll/stringcast_boost.h create mode 100644 libs/usvfs/src/usvfs_dll/usvfs.cpp create mode 100644 libs/usvfs/src/usvfs_dll/usvfsparameters.cpp create mode 100644 libs/usvfs/src/usvfs_dll/version.rc create mode 100644 libs/usvfs/src/usvfs_helper/CMakeLists.txt create mode 100644 libs/usvfs/src/usvfs_helper/inject.cpp create mode 100644 libs/usvfs/src/usvfs_helper/inject.h create mode 100644 libs/usvfs/src/usvfs_proxy/CMakeLists.txt create mode 100644 libs/usvfs/src/usvfs_proxy/main.cpp create mode 100644 libs/usvfs/src/usvfs_proxy/version.rc create mode 100644 libs/usvfs/test/CMakeLists.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/data/file.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/docs/doc.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/empty/.gitkeep create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/readme.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/data/file.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/docs/doc.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/empty/.gitkeep create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/info.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/readme.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/data/.gitkeep create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/doc.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/subdocs/.gitkeep create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/data/.gitkeep create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/doc.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/subdocs/.gitkeep create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/example.cpp create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/data/.gitkeep create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/overwrite/r6/storages/HUDPainter/TEST.json create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/data/.gitkeep create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/docs/doc.skip create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/file.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.skip create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/empty/.gitkeep create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.skip create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/overwrite/readme.skip create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/docs/doc.skip create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/file.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.skip create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/empty/.gitkeep create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.skip create mode 100644 libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rcopyme4.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfile0.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfiledeletewrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfilerewrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0w.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1w.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rcopyme4.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfile0.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledelete.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledeletewrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfileoldname.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfilerewrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0w.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1w.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfilew.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder3/mdeep3/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1w.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder2/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove2p.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite2p.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilemoveover.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfileoverwrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilerewrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mod2.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder3/mdeep3/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder4/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mod3.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletemove.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletewrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilemoveover.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfileoverwrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilerewrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/.empty create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfile1.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfolder1/newfile1.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder2/newfile2.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3e.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletemove2p.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletewrite2p.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4e.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4enr.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4r.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4p/rcopyme4.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/rcopyme4.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/rfolder/rfilenewname.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1w.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2w.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfilew.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder3/mdeep3/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1w.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder2/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove2p.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite2p.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilemoveover.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfileoverwrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilerewrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mod2.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder3/mdeep3/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder4/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mod3.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfile.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove2p.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite2p.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilemoveover.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfileoverwrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilerewrite.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/overwrite/.empty create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/root1.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/root1w.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/root2.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/source/root2w.txt create mode 100644 libs/usvfs/test/fixtures/usvfs_test/basic/vfs_mappings.txt create mode 100644 libs/usvfs/test/gtest_utils/CMakeLists.txt create mode 100644 libs/usvfs/test/gtest_utils/gtest_utils.cpp create mode 100644 libs/usvfs/test/gtest_utils/gtest_utils.h create mode 100644 libs/usvfs/test/shared_test/CMakeLists.txt create mode 100644 libs/usvfs/test/shared_test/main.cpp create mode 100644 libs/usvfs/test/test_utils/CMakeLists.txt create mode 100644 libs/usvfs/test/test_utils/test_helpers.cpp create mode 100644 libs/usvfs/test/test_utils/test_helpers.h create mode 100644 libs/usvfs/test/thooklib_test/CMakeLists.txt create mode 100644 libs/usvfs/test/thooklib_test/main.cpp create mode 100644 libs/usvfs/test/thooklib_test/test_hooks.cpp create mode 100644 libs/usvfs/test/tinjectlib_test/CMakeLists.txt create mode 100644 libs/usvfs/test/tinjectlib_test/main.cpp create mode 100644 libs/usvfs/test/tinjectlib_test/testinject_bin/main.cpp create mode 100644 libs/usvfs/test/tinjectlib_test/testinject_dll/main.cpp create mode 100644 libs/usvfs/test/tinjectlib_test/testinject_dll/main.h create mode 100644 libs/usvfs/test/tvfs_test/CMakeLists.txt create mode 100644 libs/usvfs/test/tvfs_test/main.cpp create mode 100644 libs/usvfs/test/usvfs_global_test_runner/CMakeLists.txt create mode 100644 libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test/usvfs_global_test.cpp create mode 100644 libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp create mode 100644 libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.h create mode 100644 libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp create mode 100644 libs/usvfs/test/usvfs_test_runner/CMakeLists.txt create mode 100644 libs/usvfs/test/usvfs_test_runner/test_file_operations/test_file_operations.cpp create mode 100644 libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.cpp create mode 100644 libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.h create mode 100644 libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.cpp create mode 100644 libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.h create mode 100644 libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntdll_declarations.h create mode 100644 libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.cpp create mode 100644 libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.h create mode 100644 libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.cpp create mode 100644 libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.h create mode 100644 libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test.cpp create mode 100644 libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.cpp create mode 100644 libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.h create mode 100644 libs/usvfs/test/usvfs_test_runner/usvfs_test_runner.cpp create mode 100644 libs/usvfs/vcpkg-configuration.json create mode 100644 libs/usvfs/vcpkg.json create mode 100644 libs/vcpkg-registry/.gitattributes create mode 100644 libs/vcpkg-registry/.pre-commit-config.yaml create mode 100644 libs/vcpkg-registry/README.md create mode 100644 libs/vcpkg-registry/ports/7zip/7zip-config.cmake.in create mode 100644 libs/vcpkg-registry/ports/7zip/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/7zip/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/asmjit/asmjit.patch create mode 100644 libs/vcpkg-registry/ports/asmjit/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/asmjit/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/libloot/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/libloot/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/mo2-archive/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/mo2-archive/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/mo2-bsatk/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/mo2-bsatk/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/mo2-cmake/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/mo2-cmake/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/mo2-dds-header/mo2-dds-header-config.cmake create mode 100644 libs/vcpkg-registry/ports/mo2-dds-header/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/mo2-dds-header/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/mo2-esptk/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/mo2-esptk/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/mo2-libbsarch/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/mo2-libbsarch/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/mo2-lootcli-header/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/mo2-lootcli-header/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/mo2-uibase/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/mo2-uibase/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/pybind11/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/pybind11/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/spdlog/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/spdlog/vcpkg.json create mode 100644 libs/vcpkg-registry/ports/usvfs/portfile.cmake create mode 100644 libs/vcpkg-registry/ports/usvfs/vcpkg.json create mode 100644 libs/vcpkg-registry/versions/7-/7zip.json create mode 100644 libs/vcpkg-registry/versions/a-/asmjit.json create mode 100644 libs/vcpkg-registry/versions/baseline.json create mode 100644 libs/vcpkg-registry/versions/l-/libloot.json create mode 100644 libs/vcpkg-registry/versions/m-/mo2-archive.json create mode 100644 libs/vcpkg-registry/versions/m-/mo2-bsatk.json create mode 100644 libs/vcpkg-registry/versions/m-/mo2-cmake.json create mode 100644 libs/vcpkg-registry/versions/m-/mo2-dds-header.json create mode 100644 libs/vcpkg-registry/versions/m-/mo2-esptk.json create mode 100644 libs/vcpkg-registry/versions/m-/mo2-libbsarch.json create mode 100644 libs/vcpkg-registry/versions/m-/mo2-lootcli-header.json create mode 100644 libs/vcpkg-registry/versions/m-/mo2-uibase.json create mode 100644 libs/vcpkg-registry/versions/p-/pybind11.json create mode 100644 libs/vcpkg-registry/versions/s-/spdlog.json create mode 100644 libs/vcpkg-registry/versions/u-/usvfs.json create mode 100644 libs/winreg.py create mode 100755 src/fluorine-manager create mode 100644 src/src/CMakeLists.txt create mode 100644 src/src/aboutdialog.cpp create mode 100644 src/src/aboutdialog.h create mode 100644 src/src/aboutdialog.ui create mode 100644 src/src/activatemodsdialog.cpp create mode 100644 src/src/activatemodsdialog.h create mode 100644 src/src/activatemodsdialog.ui create mode 100644 src/src/apiuseraccount.cpp create mode 100644 src/src/apiuseraccount.h create mode 100644 src/src/app_icon.rc create mode 100644 src/src/archivefiletree.cpp create mode 100644 src/src/archivefiletree.h create mode 100644 src/src/bbcode.cpp create mode 100644 src/src/bbcode.h create mode 100644 src/src/browserdialog.cpp create mode 100644 src/src/browserdialog.h create mode 100644 src/src/browserdialog.ui create mode 100644 src/src/browserview.cpp create mode 100644 src/src/browserview.h create mode 100644 src/src/categories.cpp create mode 100644 src/src/categories.h create mode 100644 src/src/categoriesdialog.cpp create mode 100644 src/src/categoriesdialog.h create mode 100644 src/src/categoriesdialog.ui create mode 100644 src/src/categoriestable.cpp create mode 100644 src/src/categoriestable.h create mode 100644 src/src/categoryimportdialog.cpp create mode 100644 src/src/categoryimportdialog.h create mode 100644 src/src/categoryimportdialog.ui create mode 100644 src/src/colortable.cpp create mode 100644 src/src/colortable.h create mode 100644 src/src/commandline.cpp create mode 100644 src/src/commandline.h create mode 100644 src/src/copyeventfilter.cpp create mode 100644 src/src/copyeventfilter.h create mode 100644 src/src/createinstancedialog.cpp create mode 100644 src/src/createinstancedialog.h create mode 100644 src/src/createinstancedialog.ui create mode 100644 src/src/createinstancedialogpages.cpp create mode 100644 src/src/createinstancedialogpages.h create mode 100644 src/src/credentialsdialog.cpp create mode 100644 src/src/credentialsdialog.h create mode 100644 src/src/credentialsdialog.ui create mode 100644 src/src/csvbuilder.cpp create mode 100644 src/src/csvbuilder.h create mode 100644 src/src/datatab.cpp create mode 100644 src/src/datatab.h create mode 100644 src/src/directoryrefresher.cpp create mode 100644 src/src/directoryrefresher.h create mode 100644 src/src/disableproxyplugindialog.cpp create mode 100644 src/src/disableproxyplugindialog.h create mode 100644 src/src/disableproxyplugindialog.ui create mode 100644 src/src/dlls.manifest.debug.qt6 create mode 100644 src/src/dlls.manifest.qt6 create mode 100644 src/src/downloadlist.cpp create mode 100644 src/src/downloadlist.h create mode 100644 src/src/downloadlistview.cpp create mode 100644 src/src/downloadlistview.h create mode 100644 src/src/downloadmanager.cpp create mode 100644 src/src/downloadmanager.h create mode 100644 src/src/downloadmanagerproxy.cpp create mode 100644 src/src/downloadmanagerproxy.h create mode 100644 src/src/downloadstab.cpp create mode 100644 src/src/downloadstab.h create mode 100644 src/src/editexecutablesdialog.cpp create mode 100644 src/src/editexecutablesdialog.h create mode 100644 src/src/editexecutablesdialog.ui create mode 100644 src/src/env.cpp create mode 100644 src/src/env.h create mode 100644 src/src/envdump.h create mode 100644 src/src/envfs.cpp create mode 100644 src/src/envfs.h create mode 100644 src/src/envmetrics.cpp create mode 100644 src/src/envmetrics.h create mode 100644 src/src/envmodule.cpp create mode 100644 src/src/envmodule.h create mode 100644 src/src/envsecurity.cpp create mode 100644 src/src/envsecurity.h create mode 100644 src/src/envshell.cpp create mode 100644 src/src/envshell.h create mode 100644 src/src/envshortcut.cpp create mode 100644 src/src/envshortcut.h create mode 100644 src/src/envwindows.cpp create mode 100644 src/src/envwindows.h create mode 100644 src/src/executableslist.cpp create mode 100644 src/src/executableslist.h create mode 100644 src/src/executableslistproxy.cpp create mode 100644 src/src/executableslistproxy.h create mode 100644 src/src/filedialogmemory.cpp create mode 100644 src/src/filedialogmemory.h create mode 100644 src/src/filerenamer.cpp create mode 100644 src/src/filerenamer.h create mode 100644 src/src/filetree.cpp create mode 100644 src/src/filetree.h create mode 100644 src/src/filetreeitem.cpp create mode 100644 src/src/filetreeitem.h create mode 100644 src/src/filetreemodel.cpp create mode 100644 src/src/filetreemodel.h create mode 100644 src/src/filterlist.cpp create mode 100644 src/src/filterlist.h create mode 100644 src/src/fluorineconfig.cpp create mode 100644 src/src/fluorineconfig.h create mode 100644 src/src/forcedloaddialog.cpp create mode 100644 src/src/forcedloaddialog.h create mode 100644 src/src/forcedloaddialog.ui create mode 100644 src/src/forcedloaddialogwidget.cpp create mode 100644 src/src/forcedloaddialogwidget.h create mode 100644 src/src/forcedloaddialogwidget.ui create mode 100644 src/src/fuseconnector.cpp create mode 100644 src/src/fuseconnector.h create mode 100644 src/src/game_features.cpp create mode 100644 src/src/game_features.h create mode 100644 src/src/gamefeaturesproxy.cpp create mode 100644 src/src/gamefeaturesproxy.h create mode 100644 src/src/genericicondelegate.cpp create mode 100644 src/src/genericicondelegate.h create mode 100644 src/src/github.cpp create mode 100644 src/src/github.h create mode 100644 src/src/glob_matching.h create mode 100644 src/src/icondelegate.cpp create mode 100644 src/src/icondelegate.h create mode 100644 src/src/iconfetcher.cpp create mode 100644 src/src/iconfetcher.h create mode 100644 src/src/installationmanager.cpp create mode 100644 src/src/installationmanager.h create mode 100644 src/src/instancemanager.cpp create mode 100644 src/src/instancemanager.h create mode 100644 src/src/instancemanagerdialog.cpp create mode 100644 src/src/instancemanagerdialog.h create mode 100644 src/src/instancemanagerdialog.ui create mode 100644 src/src/iuserinterface.h create mode 100644 src/src/json.h create mode 100644 src/src/lcdnumber.cpp create mode 100644 src/src/lcdnumber.h create mode 100644 src/src/listdialog.cpp create mode 100644 src/src/listdialog.h create mode 100644 src/src/listdialog.ui create mode 100644 src/src/loghighlighter.cpp create mode 100644 src/src/loghighlighter.h create mode 100644 src/src/loglist.cpp create mode 100644 src/src/loglist.h create mode 100644 src/src/loot.cpp create mode 100644 src/src/loot.h create mode 100644 src/src/lootdialog.cpp create mode 100644 src/src/lootdialog.h create mode 100644 src/src/lootdialog.ui create mode 100644 src/src/main.cpp create mode 100644 src/src/mainwindow.cpp create mode 100644 src/src/mainwindow.h create mode 100644 src/src/mainwindow.ui create mode 100644 src/src/messagedialog.cpp create mode 100644 src/src/messagedialog.h create mode 100644 src/src/messagedialog.ui create mode 100644 src/src/mo_icon.ico create mode 100644 src/src/moapplication.cpp create mode 100644 src/src/moapplication.h create mode 100644 src/src/modconflicticondelegate.cpp create mode 100644 src/src/modconflicticondelegate.h create mode 100644 src/src/modcontenticondelegate.cpp create mode 100644 src/src/modcontenticondelegate.h create mode 100644 src/src/modelutils.cpp create mode 100644 src/src/modelutils.h create mode 100644 src/src/modflagicondelegate.cpp create mode 100644 src/src/modflagicondelegate.h create mode 100644 src/src/modidlineedit.cpp create mode 100644 src/src/modidlineedit.h create mode 100644 src/src/modinfo.cpp create mode 100644 src/src/modinfo.h create mode 100644 src/src/modinfobackup.cpp create mode 100644 src/src/modinfobackup.h create mode 100644 src/src/modinfodialog.cpp create mode 100644 src/src/modinfodialog.h create mode 100644 src/src/modinfodialog.ui create mode 100644 src/src/modinfodialogcategories.cpp create mode 100644 src/src/modinfodialogcategories.h create mode 100644 src/src/modinfodialogconflicts.cpp create mode 100644 src/src/modinfodialogconflicts.h create mode 100644 src/src/modinfodialogconflictsmodels.cpp create mode 100644 src/src/modinfodialogconflictsmodels.h create mode 100644 src/src/modinfodialogesps.cpp create mode 100644 src/src/modinfodialogesps.h create mode 100644 src/src/modinfodialogfiletree.cpp create mode 100644 src/src/modinfodialogfiletree.h create mode 100644 src/src/modinfodialogfwd.h create mode 100644 src/src/modinfodialogimages.cpp create mode 100644 src/src/modinfodialogimages.h create mode 100644 src/src/modinfodialognexus.cpp create mode 100644 src/src/modinfodialognexus.h create mode 100644 src/src/modinfodialogtab.cpp create mode 100644 src/src/modinfodialogtab.h create mode 100644 src/src/modinfodialogtextfiles.cpp create mode 100644 src/src/modinfodialogtextfiles.h create mode 100644 src/src/modinfoforeign.cpp create mode 100644 src/src/modinfoforeign.h create mode 100644 src/src/modinfooverwrite.cpp create mode 100644 src/src/modinfooverwrite.h create mode 100644 src/src/modinforegular.cpp create mode 100644 src/src/modinforegular.h create mode 100644 src/src/modinfoseparator.cpp create mode 100644 src/src/modinfoseparator.h create mode 100644 src/src/modinfowithconflictinfo.cpp create mode 100644 src/src/modinfowithconflictinfo.h create mode 100644 src/src/modlist.cpp create mode 100644 src/src/modlist.h create mode 100644 src/src/modlistbypriorityproxy.cpp create mode 100644 src/src/modlistbypriorityproxy.h create mode 100644 src/src/modlistcontextmenu.cpp create mode 100644 src/src/modlistcontextmenu.h create mode 100644 src/src/modlistdropinfo.cpp create mode 100644 src/src/modlistdropinfo.h create mode 100644 src/src/modlistproxy.cpp create mode 100644 src/src/modlistproxy.h create mode 100644 src/src/modlistsortproxy.cpp create mode 100644 src/src/modlistsortproxy.h create mode 100644 src/src/modlistversiondelegate.cpp create mode 100644 src/src/modlistversiondelegate.h create mode 100644 src/src/modlistview.cpp create mode 100644 src/src/modlistview.h create mode 100644 src/src/modlistviewactions.cpp create mode 100644 src/src/modlistviewactions.h create mode 100644 src/src/moshortcut.cpp create mode 100644 src/src/moshortcut.h create mode 100644 src/src/motddialog.cpp create mode 100644 src/src/motddialog.h create mode 100644 src/src/motddialog.ui create mode 100644 src/src/multiprocess.cpp create mode 100644 src/src/multiprocess.h create mode 100644 src/src/nexusinterface.cpp create mode 100644 src/src/nexusinterface.h create mode 100644 src/src/nexusmanualkey.ui create mode 100644 src/src/noeditdelegate.cpp create mode 100644 src/src/noeditdelegate.h create mode 100644 src/src/nxmaccessmanager.cpp create mode 100644 src/src/nxmaccessmanager.h create mode 100644 src/src/nxmhandler_linux.cpp create mode 100644 src/src/nxmhandler_linux.h create mode 100644 src/src/organizer_en.ts create mode 100644 src/src/organizercore.cpp create mode 100644 src/src/organizercore.h create mode 100644 src/src/organizerproxy.cpp create mode 100644 src/src/organizerproxy.h create mode 100644 src/src/overwriteinfodialog.cpp create mode 100644 src/src/overwriteinfodialog.h create mode 100644 src/src/overwriteinfodialog.ui create mode 100644 src/src/pch.cpp create mode 100644 src/src/pch.h create mode 100644 src/src/persistentcookiejar.cpp create mode 100644 src/src/persistentcookiejar.h create mode 100644 src/src/plugincontainer.cpp create mode 100644 src/src/plugincontainer.h create mode 100644 src/src/pluginlist.cpp create mode 100644 src/src/pluginlist.h create mode 100644 src/src/pluginlistcontextmenu.cpp create mode 100644 src/src/pluginlistcontextmenu.h create mode 100644 src/src/pluginlistproxy.cpp create mode 100644 src/src/pluginlistproxy.h create mode 100644 src/src/pluginlistsortproxy.cpp create mode 100644 src/src/pluginlistsortproxy.h create mode 100644 src/src/pluginlistview.cpp create mode 100644 src/src/pluginlistview.h create mode 100644 src/src/previewdialog.cpp create mode 100644 src/src/previewdialog.h create mode 100644 src/src/previewdialog.ui create mode 100644 src/src/previewgenerator.cpp create mode 100644 src/src/previewgenerator.h create mode 100644 src/src/problemsdialog.cpp create mode 100644 src/src/problemsdialog.h create mode 100644 src/src/problemsdialog.ui create mode 100644 src/src/processrunner.cpp create mode 100644 src/src/processrunner.h create mode 100644 src/src/profile.cpp create mode 100644 src/src/profile.h create mode 100644 src/src/profileinputdialog.cpp create mode 100644 src/src/profileinputdialog.h create mode 100644 src/src/profileinputdialog.ui create mode 100644 src/src/profilesdialog.cpp create mode 100644 src/src/profilesdialog.h create mode 100644 src/src/profilesdialog.ui create mode 100644 src/src/protonlauncher.cpp create mode 100644 src/src/protonlauncher.h create mode 100644 src/src/proxyutils.h create mode 100644 src/src/qdirfiletree.cpp create mode 100644 src/src/qdirfiletree.h create mode 100644 src/src/qt.conf create mode 100644 src/src/qtgroupingproxy.cpp create mode 100644 src/src/qtgroupingproxy.h create mode 100644 src/src/queryoverwritedialog.cpp create mode 100644 src/src/queryoverwritedialog.h create mode 100644 src/src/queryoverwritedialog.ui create mode 100644 src/src/resources.qrc create mode 100644 src/src/resources/accessories-text-editor.png create mode 100644 src/src/resources/application-x-executable.png create mode 100644 src/src/resources/applications-accessories.png create mode 100644 src/src/resources/archive-conflict-loser.png create mode 100644 src/src/resources/archive-conflict-mixed.png create mode 100644 src/src/resources/archive-conflict-neutral.png create mode 100644 src/src/resources/archive-conflict-winner.png create mode 100644 src/src/resources/arrange-boxes.png create mode 100644 src/src/resources/badge_1.png create mode 100644 src/src/resources/badge_2.png create mode 100644 src/src/resources/badge_3.png create mode 100644 src/src/resources/badge_4.png create mode 100644 src/src/resources/badge_5.png create mode 100644 src/src/resources/badge_6.png create mode 100644 src/src/resources/badge_7.png create mode 100644 src/src/resources/badge_8.png create mode 100644 src/src/resources/badge_9.png create mode 100644 src/src/resources/badge_more.png create mode 100644 src/src/resources/branch-closed.png create mode 100644 src/src/resources/branch-open.png create mode 100644 src/src/resources/check.png create mode 100644 src/src/resources/combobox-down.png create mode 100644 src/src/resources/conflict-hidden.png create mode 100644 src/src/resources/conflict-mixed-blue.png create mode 100644 src/src/resources/conflict-mixed.png create mode 100644 src/src/resources/conflict-overwrite-blue.png create mode 100644 src/src/resources/conflict-overwrite.png create mode 100644 src/src/resources/conflict-overwritten-blue.png create mode 100644 src/src/resources/conflict-overwritten.png create mode 100644 src/src/resources/conflict-redundant.png create mode 100644 src/src/resources/contact-new.png create mode 100644 src/src/resources/contents/breastplate.png create mode 100644 src/src/resources/contents/checkbox-tree.png create mode 100644 src/src/resources/contents/config.png create mode 100644 src/src/resources/contents/conversation.png create mode 100644 src/src/resources/contents/double-quaver.png create mode 100644 src/src/resources/contents/empty-chessboard.png create mode 100644 src/src/resources/contents/f4-nuclear.png create mode 100644 src/src/resources/contents/facegen.png create mode 100644 src/src/resources/contents/feather-and-scroll.png create mode 100644 src/src/resources/contents/hand-of-god.png create mode 100644 src/src/resources/contents/jigsaw-piece.png create mode 100644 src/src/resources/contents/locked-chest.png create mode 100644 src/src/resources/contents/lyre.png create mode 100644 src/src/resources/contents/media.png create mode 100644 src/src/resources/contents/mesh-box.png create mode 100644 src/src/resources/contents/tinker.png create mode 100644 src/src/resources/contents/usable.png create mode 100644 src/src/resources/contents/xedit.png create mode 100644 src/src/resources/dialog-information.png create mode 100644 src/src/resources/dialog-warning.png create mode 100644 src/src/resources/dialog-warning_16.png create mode 100644 src/src/resources/dialog-warning_16.png (warning_16) create mode 100644 src/src/resources/document-properties.png create mode 100644 src/src/resources/document-save.png create mode 100644 src/src/resources/document-save_32.png create mode 100644 src/src/resources/dynamic-blue-right.png create mode 100644 src/src/resources/edit-clear.png create mode 100644 src/src/resources/edit-find-replace.png create mode 100644 src/src/resources/edit-undo.png create mode 100644 src/src/resources/emblem-favorite - 64.png create mode 100644 src/src/resources/emblem-favorite.png create mode 100644 src/src/resources/emblem-important.png create mode 100644 src/src/resources/emblem-readonly.png create mode 100644 src/src/resources/emblem-redundant.png create mode 100644 src/src/resources/emblem-unreadable.png create mode 100644 src/src/resources/error.png create mode 100644 src/src/resources/function.png create mode 100644 src/src/resources/game-warning-16.png create mode 100644 src/src/resources/game-warning.png create mode 100644 src/src/resources/go-down.png create mode 100644 src/src/resources/go-jump.png create mode 100644 src/src/resources/go-next_16.png create mode 100644 src/src/resources/go-previous_16.png create mode 100644 src/src/resources/go-up.png create mode 100644 src/src/resources/help-browser.png create mode 100644 src/src/resources/help-browser_32.png create mode 100644 src/src/resources/icon-favorite.png create mode 100644 src/src/resources/icons8-checked-checkbox.png create mode 100644 src/src/resources/icons8-indeterminate-checkbox.png create mode 100644 src/src/resources/icons8-unchecked-checkbox.png create mode 100644 src/src/resources/internet-web-browser.png create mode 100644 src/src/resources/list-add.png create mode 100644 src/src/resources/list-remove.png create mode 100644 src/src/resources/log-debug.png create mode 100644 src/src/resources/mail-attachment.png create mode 100644 src/src/resources/markdown.html create mode 100644 src/src/resources/media-playback-start.png create mode 100644 src/src/resources/mo_icon.png create mode 100644 src/src/resources/multiply-red.png create mode 100644 src/src/resources/open-Folder-Icon.png create mode 100644 src/src/resources/package.png create mode 100644 src/src/resources/plugins.png create mode 100644 src/src/resources/preferences-system.png create mode 100644 src/src/resources/process-stop.png create mode 100644 src/src/resources/red-archive-conflict-loser.png create mode 100644 src/src/resources/save.svg create mode 100644 src/src/resources/show.png create mode 100644 src/src/resources/software-update-available.png create mode 100644 src/src/resources/start-here.png create mode 100644 src/src/resources/status_active.png create mode 100644 src/src/resources/status_awaiting.png create mode 100644 src/src/resources/status_inactive.png create mode 100644 src/src/resources/switch-instance-icon.png create mode 100644 src/src/resources/symbol-backup.png create mode 100644 src/src/resources/system-installer.png create mode 100644 src/src/resources/system-search.png create mode 100644 src/src/resources/system-software-update.png create mode 100644 src/src/resources/tracked.png create mode 100644 src/src/resources/view-refresh.png create mode 100644 src/src/resources/view-refresh_16.png create mode 100644 src/src/resources/word-wrap.svg create mode 100644 src/src/resources/x-office-calendar.png create mode 100644 src/src/sanitychecks.cpp create mode 100644 src/src/sanitychecks.h create mode 100644 src/src/savestab.cpp create mode 100644 src/src/savestab.h create mode 100644 src/src/savetextasdialog.cpp create mode 100644 src/src/savetextasdialog.h create mode 100644 src/src/savetextasdialog.ui create mode 100644 src/src/selectiondialog.cpp create mode 100644 src/src/selectiondialog.h create mode 100644 src/src/selectiondialog.ui create mode 100644 src/src/selfupdater.cpp create mode 100644 src/src/selfupdater.h create mode 100644 src/src/serverinfo.cpp create mode 100644 src/src/serverinfo.h create mode 100644 src/src/settings.cpp create mode 100644 src/src/settings.h create mode 100644 src/src/settingsdialog.cpp create mode 100644 src/src/settingsdialog.h create mode 100644 src/src/settingsdialog.ui create mode 100644 src/src/settingsdialogdiagnostics.cpp create mode 100644 src/src/settingsdialogdiagnostics.h create mode 100644 src/src/settingsdialoggeneral.cpp create mode 100644 src/src/settingsdialoggeneral.h create mode 100644 src/src/settingsdialogmodlist.cpp create mode 100644 src/src/settingsdialogmodlist.h create mode 100644 src/src/settingsdialognexus.cpp create mode 100644 src/src/settingsdialognexus.h create mode 100644 src/src/settingsdialogpaths.cpp create mode 100644 src/src/settingsdialogpaths.h create mode 100644 src/src/settingsdialogplugins.cpp create mode 100644 src/src/settingsdialogplugins.h create mode 100644 src/src/settingsdialogproton.cpp create mode 100644 src/src/settingsdialogproton.h create mode 100644 src/src/settingsdialogtheme.cpp create mode 100644 src/src/settingsdialogtheme.h create mode 100644 src/src/settingsdialogworkarounds.cpp create mode 100644 src/src/settingsdialogworkarounds.h create mode 100644 src/src/settingsutilities.cpp create mode 100644 src/src/settingsutilities.h create mode 100644 src/src/shared/appconfig.cpp create mode 100644 src/src/shared/appconfig.h create mode 100644 src/src/shared/appconfig.inc create mode 100644 src/src/shared/directoryentry.cpp create mode 100644 src/src/shared/directoryentry.h create mode 100644 src/src/shared/fileentry.cpp create mode 100644 src/src/shared/fileentry.h create mode 100644 src/src/shared/fileregister.cpp create mode 100644 src/src/shared/fileregister.h create mode 100644 src/src/shared/fileregisterfwd.h create mode 100644 src/src/shared/filesorigin.cpp create mode 100644 src/src/shared/filesorigin.h create mode 100644 src/src/shared/originconnection.cpp create mode 100644 src/src/shared/originconnection.h create mode 100644 src/src/shared/util.cpp create mode 100644 src/src/shared/util.h create mode 100644 src/src/shared/windows_compat.h create mode 100644 src/src/shared/windows_error.cpp create mode 100644 src/src/shared/windows_error.h create mode 100644 src/src/spawn.cpp create mode 100644 src/src/spawn.h create mode 100644 src/src/splash.png create mode 100644 src/src/statusbar.cpp create mode 100644 src/src/statusbar.h create mode 100644 src/src/stylesheet_resource.qrc create mode 100644 src/src/stylesheets/Night Eyes.qss create mode 100644 src/src/stylesheets/Parchment v1.1 by Bob.qss create mode 100644 src/src/stylesheets/Parchment/checkbox-alt-checked.png create mode 100644 src/src/stylesheets/Parchment/checkbox-alt-unchecked-hover.png create mode 100644 src/src/stylesheets/Parchment/checkbox-alt-unchecked.png create mode 100644 src/src/stylesheets/Parchment/checkbox-checked-disabled.png create mode 100644 src/src/stylesheets/Parchment/checkbox-checked-hover.png create mode 100644 src/src/stylesheets/Parchment/checkbox-checked.png create mode 100644 src/src/stylesheets/Parchment/checkbox-disabled.png create mode 100644 src/src/stylesheets/Parchment/checkbox-hover.png create mode 100644 src/src/stylesheets/Parchment/checkbox.png create mode 100644 src/src/stylesheets/dark.qss create mode 100644 src/src/stylesheets/dracula.qss create mode 100644 src/src/stylesheets/skyrim.qss create mode 100644 src/src/stylesheets/skyrim/arrow-down.png create mode 100644 src/src/stylesheets/skyrim/arrow-left.png create mode 100644 src/src/stylesheets/skyrim/arrow-right.png create mode 100644 src/src/stylesheets/skyrim/arrow-up.png create mode 100644 src/src/stylesheets/skyrim/border-image.png create mode 100644 src/src/stylesheets/skyrim/border-image1.png create mode 100644 src/src/stylesheets/skyrim/border-image2.png create mode 100644 src/src/stylesheets/skyrim/branch-opened.png create mode 100644 src/src/stylesheets/skyrim/button-big-border.png create mode 100644 src/src/stylesheets/skyrim/button-border.png create mode 100644 src/src/stylesheets/skyrim/button-checked-border.png create mode 100644 src/src/stylesheets/skyrim/checkbox-alt-checked.png create mode 100644 src/src/stylesheets/skyrim/checkbox-alt-unchecked-hover.png create mode 100644 src/src/stylesheets/skyrim/checkbox-alt-unchecked.png create mode 100644 src/src/stylesheets/skyrim/checkbox-checked-disabled.png create mode 100644 src/src/stylesheets/skyrim/checkbox-checked-hover.png create mode 100644 src/src/stylesheets/skyrim/checkbox-checked.png create mode 100644 src/src/stylesheets/skyrim/checkbox-disabled.png create mode 100644 src/src/stylesheets/skyrim/checkbox-hover.png create mode 100644 src/src/stylesheets/skyrim/checkbox.png create mode 100644 src/src/stylesheets/skyrim/context-menu-separator.png create mode 100644 src/src/stylesheets/skyrim/progress-bar-border.png create mode 100644 src/src/stylesheets/skyrim/progress-bar-chunk.png create mode 100644 src/src/stylesheets/skyrim/radio-checked.png create mode 100644 src/src/stylesheets/skyrim/radio-hover.png create mode 100644 src/src/stylesheets/skyrim/radio.png create mode 100644 src/src/stylesheets/skyrim/scrollbar-down.png create mode 100644 src/src/stylesheets/skyrim/scrollbar-horizontal.png create mode 100644 src/src/stylesheets/skyrim/scrollbar-left.png create mode 100644 src/src/stylesheets/skyrim/scrollbar-right.png create mode 100644 src/src/stylesheets/skyrim/scrollbar-up.png create mode 100644 src/src/stylesheets/skyrim/scrollbar-vertical.png create mode 100644 src/src/stylesheets/skyrim/separator.png create mode 100644 src/src/stylesheets/skyrim/slider-border.png create mode 100644 src/src/stylesheets/skyrim/slider-handle.png create mode 100644 src/src/stylesheets/vs15 Dark-Green.qss create mode 100644 src/src/stylesheets/vs15 Dark-Orange.qss create mode 100644 src/src/stylesheets/vs15 Dark-Pink.qss create mode 100644 src/src/stylesheets/vs15 Dark-Purple.qss create mode 100644 src/src/stylesheets/vs15 Dark-Red.qss create mode 100644 src/src/stylesheets/vs15 Dark-Yellow.qss create mode 100644 src/src/stylesheets/vs15 Dark.qss create mode 100644 src/src/stylesheets/vs15/branch-closed.png create mode 100644 src/src/stylesheets/vs15/branch-open.png create mode 100644 src/src/stylesheets/vs15/checkbox-check-disabled.png create mode 100644 src/src/stylesheets/vs15/checkbox-check.png create mode 100644 src/src/stylesheets/vs15/combobox-down.png create mode 100644 src/src/stylesheets/vs15/scrollbar-down-disabled.png create mode 100644 src/src/stylesheets/vs15/scrollbar-down-hover.png create mode 100644 src/src/stylesheets/vs15/scrollbar-down.png create mode 100644 src/src/stylesheets/vs15/scrollbar-left-disabled.png create mode 100644 src/src/stylesheets/vs15/scrollbar-left-hover.png create mode 100644 src/src/stylesheets/vs15/scrollbar-left.png create mode 100644 src/src/stylesheets/vs15/scrollbar-right-disabled.png create mode 100644 src/src/stylesheets/vs15/scrollbar-right-hover.png create mode 100644 src/src/stylesheets/vs15/scrollbar-right.png create mode 100644 src/src/stylesheets/vs15/scrollbar-up-disabled.png create mode 100644 src/src/stylesheets/vs15/scrollbar-up-hover.png create mode 100644 src/src/stylesheets/vs15/scrollbar-up.png create mode 100644 src/src/stylesheets/vs15/sort-asc.png create mode 100644 src/src/stylesheets/vs15/sort-desc.png create mode 100644 src/src/stylesheets/vs15/spinner-down.png create mode 100644 src/src/stylesheets/vs15/spinner-up.png create mode 100644 src/src/stylesheets/vs15/sub-menu-arrow-hover.png create mode 100644 src/src/stylesheets/vs15/sub-menu-arrow.png create mode 100644 src/src/syncoverwritedialog.cpp create mode 100644 src/src/syncoverwritedialog.h create mode 100644 src/src/syncoverwritedialog.ui create mode 100644 src/src/systemtraymanager.cpp create mode 100644 src/src/systemtraymanager.h create mode 100644 src/src/texteditor.cpp create mode 100644 src/src/texteditor.h create mode 100644 src/src/thread_utils.h create mode 100644 src/src/transfersavesdialog.cpp create mode 100644 src/src/transfersavesdialog.h create mode 100644 src/src/transfersavesdialog.ui create mode 100644 src/src/tutorials/Highlight.qml create mode 100644 src/src/tutorials/Tooltip.qml create mode 100644 src/src/tutorials/TooltipArea.qml create mode 100644 src/src/tutorials/TutorialCanceller.qml create mode 100644 src/src/tutorials/TutorialDescription.qml create mode 100644 src/src/tutorials/TutorialOverlay.qml create mode 100644 src/src/tutorials/tutorial_basics.js create mode 100644 src/src/tutorials/tutorial_conflictresolution_main.js create mode 100644 src/src/tutorials/tutorial_conflictresolution_modinfo.js create mode 100644 src/src/tutorials/tutorial_firststeps.js create mode 100644 src/src/tutorials/tutorial_firststeps_browser.js create mode 100644 src/src/tutorials/tutorial_firststeps_main.js create mode 100644 src/src/tutorials/tutorial_firststeps_modinfo.js create mode 100644 src/src/tutorials/tutorial_firststeps_settings.js create mode 100644 src/src/tutorials/tutorial_primer_main.js create mode 100644 src/src/tutorials/tutorial_window_installer.js create mode 100644 src/src/tutorials/tutorials.js create mode 100644 src/src/tutorials/tutorials_installdialog.qml create mode 100644 src/src/tutorials/tutorials_mainwindow.qml create mode 100644 src/src/tutorials/tutorials_modinfodialog.qml create mode 100644 src/src/tutorials/tutorials_nexusdialog.qml create mode 100644 src/src/tutorials/tutorials_settingsdialog.qml create mode 100644 src/src/uilocker.cpp create mode 100644 src/src/uilocker.h create mode 100644 src/src/updatedialog.cpp create mode 100644 src/src/updatedialog.h create mode 100644 src/src/updatedialog.ui create mode 100644 src/src/usvfsconnector.cpp create mode 100644 src/src/usvfsconnector.h create mode 100644 src/src/validationprogressdialog.ui create mode 100644 src/src/version.rc create mode 100644 src/src/vfs/inodetable.cpp create mode 100644 src/src/vfs/inodetable.h create mode 100644 src/src/vfs/mo2filesystem.cpp create mode 100644 src/src/vfs/mo2filesystem.h create mode 100644 src/src/vfs/overwritemanager.cpp create mode 100644 src/src/vfs/overwritemanager.h create mode 100644 src/src/vfs/vfs_helper_main.cpp create mode 100644 src/src/vfs/vfstree.cpp create mode 100644 src/src/vfs/vfstree.h create mode 100644 src/src/viewmarkingscrollbar.cpp create mode 100644 src/src/viewmarkingscrollbar.h create mode 100644 src/src/virtualfiletree.cpp create mode 100644 src/src/virtualfiletree.h create mode 100644 src/src/wineprefix.cpp create mode 100644 src/src/wineprefix.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9f0e11b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,193 @@ +name: CI + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + workflow_dispatch: + +env: + CCACHE_DIR: /__w/.ccache + CCACHE_BASEDIR: /__w/Fluorine-Manager/Fluorine-Manager + CCACHE_COMPILERCHECK: content + CCACHE_NOHASHDIR: "1" + CCACHE_SLOPPINESS: time_macros + CCACHE_MAXSIZE: 2G + CMAKE_BUILD_TYPE: RelWithDebInfo + +jobs: + build: + runs-on: ubuntu-latest + container: + image: ghcr.io/sulfurnitride/fluorine-builder:latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Restore ccache + uses: actions/cache@v4 + with: + path: /__w/.ccache + key: ccache-v1-${{ github.ref_name }}-${{ hashFiles('CMakeLists.txt', '**/CMakeLists.txt') }} + restore-keys: | + ccache-v1-${{ github.ref_name }}- + ccache-v1- + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + libs/nak_ffi + libs/bsa_ffi + + - name: Configure + run: | + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DPython3_EXECUTABLE="$(command -v python3)" \ + -DBUILD_PLUGIN_PYTHON=ON + + - name: Build + run: cmake --build build --parallel + + - name: Build AppImage + run: | + set -euo pipefail + + MODORG_BIN="$(find build -type f -name ModOrganizer | head -n1)" + if [ -z "${MODORG_BIN}" ]; then + echo "::error::ModOrganizer binary not found under build/" + exit 1 + fi + RUNDIR="$(dirname "${MODORG_BIN}")" + + # ── Populate AppDir ── + APPDIR="${GITHUB_WORKSPACE}/AppDir" + rm -rf "${APPDIR}" + mkdir -p "${APPDIR}/usr/bin" "${APPDIR}/usr/plugins" "${APPDIR}/usr/dlls" "${APPDIR}/usr/python" + + cp -f "${RUNDIR}/ModOrganizer" "${APPDIR}/usr/bin/ModOrganizer" + [ -f "${RUNDIR}/umu-run" ] && cp -f "${RUNDIR}/umu-run" "${APPDIR}/usr/bin/" + [ -f "${RUNDIR}/README-PORTABLE.txt" ] && cp -f "${RUNDIR}/README-PORTABLE.txt" "${APPDIR}/usr/bin/" + + # Gather built plugin .so files + find build/libs -type f \( \ + -name 'libgame_*.so' -o \ + -name 'libinstaller_*.so' -o \ + -name 'libpreview_*.so' -o \ + -name 'libdiagnose_*.so' -o \ + -name 'libcheck_*.so' -o \ + -name 'libtool_*.so' -o \ + -name 'libinieditor.so' -o \ + -name 'libinibakery.so' -o \ + -name 'libbsa_extractor.so' -o \ + -name 'libbsa_packer.so' -o \ + -name 'libproxy.so' -o \ + -name 'librunner.so' \ + \) -exec cp -f {} "${APPDIR}/usr/plugins/" \; + + # Python plugin payload + for f in libplugin_python.so lzokay.py winreg.py pyCfg.py \ + DDSPreview.py Form43Checker.py ScriptExtenderPluginChecker.py; do + [ -f "build/src/src/plugins/${f}" ] && cp -f "build/src/src/plugins/${f}" "${APPDIR}/usr/plugins/" + done + for d in basic_games data libs dlls; do + [ -d "build/src/src/plugins/${d}" ] && cp -a "build/src/src/plugins/${d}" "${APPDIR}/usr/plugins/" + done + rm -f "${APPDIR}/usr/plugins/FNIS"*.py + + # 7z runtime + SO7="$(find build -type f -name 7z.so | head -n1 || true)" + if [ -n "${SO7}" ]; then + cp -f "${SO7}" "${APPDIR}/usr/dlls/7z.so" + cp -f "${SO7}" "${APPDIR}/usr/dlls/7zip.dll" + fi + + # Python runtime payload + [ -d build/src/src/python ] && cp -a build/src/src/python/. "${APPDIR}/usr/python/" + + # icoutils + for tool in wrestool icotool; do + command -v "${tool}" >/dev/null 2>&1 && cp -f "$(command -v "${tool}")" "${APPDIR}/usr/bin/" + done + + # Desktop metadata + printf '%s\n' \ + '[Desktop Entry]' \ + 'Type=Application' \ + 'Name=ModOrganizer' \ + 'Exec=ModOrganizer' \ + 'Icon=modorganizer' \ + 'Categories=Game;' \ + 'Terminal=false' \ + > "${APPDIR}/ModOrganizer.desktop" + cp -f src/src/resources/mo_icon.png "${APPDIR}/modorganizer.png" + + # ── Run linuxdeploy ── + export PATH="/opt/linuxdeploy:${PATH}" + export LINUXDEPLOY_PLUGIN_QT="/opt/linuxdeploy/linuxdeploy-plugin-qt.AppDir/AppRun" + export QMAKE=/usr/bin/qmake6 + export NO_STRIP=1 + export EXTRA_QT_PLUGINS="waylandcompositor;wayland-decoration-client;wayland-graphics-integration-client;wayland-shell-integration" + + /opt/linuxdeploy/linuxdeploy-x86_64.AppImage --appimage-extract-and-run \ + --appdir "${APPDIR}" \ + --desktop-file "${APPDIR}/ModOrganizer.desktop" \ + --icon-file "${APPDIR}/modorganizer.png" \ + --executable "${APPDIR}/usr/bin/ModOrganizer" \ + --plugin qt \ + --output appimage + + # ── Verify + collect artifact ── + APPIMAGE_FILE="$(find "${GITHUB_WORKSPACE}" -maxdepth 1 -type f -name '*.AppImage' | head -n1)" + if [ -z "${APPIMAGE_FILE}" ]; then + echo "::error::AppImage was not produced" + exit 1 + fi + + # Sanity checks + copy_relocs="$(readelf -r "${APPDIR}/usr/bin/ModOrganizer" | grep "R_X86_64_COPY" || true)" + if [ -n "${copy_relocs}" ]; then + echo "::error::ModOrganizer contains COPY relocations" + echo "${copy_relocs}" + exit 1 + fi + + mkdir -p artifact + mv -f "${APPIMAGE_FILE}" "artifact/ModOrganizer-x86_64.AppImage" + echo "AppImage size: $(du -sh artifact/)" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ModOrganizer-AppImage + path: artifact/ + if-no-files-found: error + + - name: ccache stats + if: always() + run: ccache -sv || true + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: ModOrganizer-AppImage + path: artifact/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: artifact/ModOrganizer-x86_64.AppImage + generate_release_notes: true diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..ec101c1 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,45 @@ +name: Build Docker Image + +on: + push: + branches: [main] + paths: + - 'docker/**' + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/fluorine-builder + +jobs: + build-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: docker + file: docker/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da898a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Build artifacts +build/ +build-container/ +build-container-2404/ +build-container-2404-gcc14/ + +# Rust build artifacts +target/ + +# Flatpak build cache and artifacts +.flatpak-builder/ +.flatpak-build/ +flatpak-repo/ +flatpak/builddir/ +*.flatpak + +# Logs +*.log + +# IDE / editor +.vscode/ +.idea/ +compile_commands.json +.cache/ + +# OS junk +.DS_Store +Thumbs.db diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9a688f7 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,204 @@ +cmake_minimum_required(VERSION 3.16) +project(ModOrganizer2 VERSION 2.5.3 LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Prevent COPY relocations against protected Qt6 symbols on modern distros +# (Fedora 41+, etc.) that set GNU_PROPERTY_1_NEEDED_INDIRECT_EXTERN_ACCESS. +include(CheckCXXCompilerFlag) +check_cxx_compiler_flag(-mno-direct-extern-access HAS_NO_DIRECT_EXTERN_ACCESS) +if(HAS_NO_DIRECT_EXTERN_ACCESS) + add_compile_options(-mno-direct-extern-access) +endif() + +option(BUILD_TESTING "Build tests" OFF) +option(BUILD_NAK_FFI "Build NaK Rust FFI library" ON) +option(BUILD_BSA_FFI "Build Rust BSA/BA2 FFI library" ON) +option(BUILD_DOTNET_PLUGINS "Build C++/CLI .NET plugins (Windows+MSVC only)" OFF) +option(BUILD_PLUGIN_PYTHON "Build plugin_python bridge on Linux" ON) + +# Compatibility helpers for upstream MO2 plugin repos that expect mo2-cmake. +if(NOT COMMAND mo2_configure_plugin) + function(mo2_configure_plugin target) + cmake_parse_arguments(MO2 "NO_SOURCES" "" "" ${ARGN}) + + set_target_properties(${target} PROPERTIES + AUTOMOC ON + AUTOUIC ON + AUTORCC ON + CXX_STANDARD 23 + CXX_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON + ) + + if(NOT MO2_NO_SOURCES) + file(GLOB _mo2_plugin_sources CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc + ) + if(_mo2_plugin_sources) + target_sources(${target} PRIVATE ${_mo2_plugin_sources}) + endif() + endif() + + target_compile_options(${target} PRIVATE + "-idirafter" "${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" + ) + set_property(TARGET ${target} APPEND PROPERTY AUTOMOC_MOC_OPTIONS + "-I${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" + ) + endfunction() +endif() + +if(NOT COMMAND mo2_install_plugin) + function(mo2_install_plugin target) + install(TARGETS ${target} + LIBRARY DESTINATION plugins + ) + endfunction() +endif() + +if(NOT COMMAND mo2_default_source_group) + function(mo2_default_source_group) + # no-op + endfunction() +endif() + +if(NOT COMMAND mo2_configure_target) + function(mo2_configure_target target) + set_target_properties(${target} PROPERTIES + AUTOMOC ON + AUTOUIC ON + AUTORCC ON + CXX_STANDARD 23 + CXX_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON + ) + target_compile_options(${target} PRIVATE + "-idirafter" "${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" + ) + set_property(TARGET ${target} APPEND PROPERTY AUTOMOC_MOC_OPTIONS + "-I${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" + ) + endfunction() +endif() + +if(NOT COMMAND mo2_target_sources) + function(mo2_target_sources target) + set(_scope PRIVATE) + set(_out) + set(_skip_next OFF) + foreach(_arg IN LISTS ARGN) + if(_skip_next) + set(_skip_next OFF) + continue() + endif() + if(_arg STREQUAL "FOLDER") + set(_skip_next ON) + continue() + endif() + if(_arg STREQUAL "PRIVATE" OR _arg STREQUAL "PUBLIC" OR _arg STREQUAL "INTERFACE") + set(_scope "${_arg}") + continue() + endif() + list(APPEND _out "${_arg}") + endforeach() + if(_out) + target_sources(${target} ${_scope} ${_out}) + endif() + endfunction() +endif() + +if(NOT COMMAND mo2_find_python_executable) + function(mo2_find_python_executable out_var) + if(DEFINED Python_EXECUTABLE AND Python_EXECUTABLE) + set(${out_var} "${Python_EXECUTABLE}" PARENT_SCOPE) + return() + endif() + find_program(_mo2_python_exe NAMES python3 python) + set(${out_var} "${_mo2_python_exe}" PARENT_SCOPE) + endfunction() +endif() + +if(NOT COMMAND mo2_add_translations) + function(mo2_add_translations) + # no-op on Linux port for now + endfunction() +endif() + +if(NOT COMMAND mo2_python_install_pyqt) + function(mo2_python_install_pyqt) + # Use system/venv PyQt6 directly; no bundled wheel install step. + endfunction() +endif() + +# Make dds-header findable by bsatk and libbsarch +set(mo2-dds-header_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libs/dds-header") + +# Sub-libraries in dependency order +add_subdirectory(libs/esptk) # static lib, no deps +add_subdirectory(libs/bsatk) # static lib, needs dds-header + boost + zlib + lz4 +add_subdirectory(libs/libbsarch) # shared + static, needs dds-header +add_subdirectory(libs/7zip) # builds 7z.so from source (Linux only) +add_subdirectory(libs/archive) # shared lib, needs dl +add_subdirectory(libs/lootcli) # executable, needs Qt6 + boost + libloot + curl +add_subdirectory(libs/uibase) # shared lib, needs Qt6 + spdlog + +if(BUILD_NAK_FFI) + add_subdirectory(libs/nak_ffi) +endif() + +if(BUILD_BSA_FFI) + add_subdirectory(libs/bsa_ffi) +endif() + +if(BUILD_PLUGIN_PYTHON) + set(_mo2_pyvenv_python "${CMAKE_SOURCE_DIR}/build/pyvenv/bin/python") + if(EXISTS "${_mo2_pyvenv_python}") + set(Python_EXECUTABLE "${_mo2_pyvenv_python}" CACHE FILEPATH + "Python executable for plugin_python build" FORCE) + set(pybind11_DIR + "${CMAKE_SOURCE_DIR}/build/pyvenv/lib/python3.14/site-packages/pybind11/share/cmake/pybind11" + CACHE PATH "pybind11 cmake dir for plugin_python" FORCE) + set(MO2_PYLIBS_DIR "${CMAKE_SOURCE_DIR}/build/pyvenv" + CACHE PATH "Python runtime root for plugin_python" FORCE) + endif() + add_subdirectory(libs/plugin_python) +endif() + +# Game plugin chain: features (headers) → gamebryo (static) → creation (static) → game .so plugins +add_subdirectory(libs/game_features) +add_subdirectory(libs/game_bethesda) +add_subdirectory(libs/installer_fomod) +add_subdirectory(libs/installer_bain) +add_subdirectory(libs/installer_bundle) +add_subdirectory(libs/installer_manual) +add_subdirectory(libs/installer_quick) +add_subdirectory(libs/preview_base) +add_subdirectory(libs/preview_bsa) +add_subdirectory(libs/bsa_extractor) +add_subdirectory(libs/bsapacker) +add_subdirectory(libs/diagnose_basic) +add_subdirectory(libs/check_fnis) +add_subdirectory(libs/tool_inibakery) +add_subdirectory(libs/tool_inieditor) + +if(BUILD_DOTNET_PLUGINS) + if(WIN32 AND MSVC) + add_subdirectory(libs/installer_fomod_csharp) + add_subdirectory(libs/installer_omod) + else() + message(WARNING + "BUILD_DOTNET_PLUGINS was enabled, but installer_fomod_csharp/installer_omod " + "are C++/CLI targets that require Windows + MSVC. Skipping on this platform." + ) + endif() +endif() + +# Main organizer executable +add_subdirectory(src/src) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..8e45945 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 SulfurNitride + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd849fa --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Fluorine Manager + +Fluorine Manager an attempt at porting MO2 to linux with FUSE as the VFS system. + +## Current Status + +- Core app builds and runs on Linux. +- NaK integration is wired for game/proton detection and dependency handling. +- Linux-native game plugins (`libgame_*.so`) are supported. +- Portable instances are supported via local `ModOrganizer.ini` detection. + +## FUSE Permissions + +- Users only need to change `/etc/fuse.conf` when MO2 mounts with `allow_other` (or `allow_root`). +- If `allow_other` is used, uncomment `user_allow_other` in `/etc/fuse.conf` once (system-wide). + +## Example + +`#user_allow_other` to `user_allow_other` if its missing please add it. + +## Installing and Running +Download the latest flatpak from the [releases](https://github.com/SulfurNitride/Fluorine-Manager/releases) and after you download it. + +You are able to install it with this command: `flatpak install --user fluorine-manager.flatpak` + +You can then get started with: `flatpak run com.fluorine.manager` or you should be able to find it in your apps drawer. + +More information can be found in the [FAQ](https://github.com/SulfurNitride/Fluorine-Manager/blob/main/docs/FAQ.md). + +## Build + +```bash +cmake -B build +cmake --build build -j"$(nproc)" +``` + +## Known Limitations + +- Some third-party MO2 plugins are Windows-only and will fail on Linux (for example DLL/ctypes `windll` assumptions). +- Themes are currently not working as intended. + +## Project Layout + +```text +libs/ MO2 sub-libraries +src/ Main organizer source +docs/ Notes and tracking +``` diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..2d0d4d0 --- /dev/null +++ b/build.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +IMAGE_NAME="fluorine-builder" +CONTAINER_ENGINE="${CONTAINER_ENGINE:-podman}" + +# ── Build the container image if it doesn't exist ── +if ! ${CONTAINER_ENGINE} image exists "${IMAGE_NAME}" 2>/dev/null; then + echo "Building ${IMAGE_NAME} image (one-time)..." + ${CONTAINER_ENGINE} build -t "${IMAGE_NAME}" -f "${SCRIPT_DIR}/docker/Dockerfile" "${SCRIPT_DIR}/docker" +fi + +# ── Ensure build dir exists (Podman needs it before mounting) ── +mkdir -p "${SCRIPT_DIR}/build-container" + +# ── Run the build inside the container ── +echo "Building ModOrganizer inside container..." +${CONTAINER_ENGINE} run --rm \ + -v "${SCRIPT_DIR}:/src:Z" \ + -v "${SCRIPT_DIR}/build-container:/src/build:Z" \ + -w /src \ + "${IMAGE_NAME}" \ + bash docker/build-inner.sh + +echo "" +echo "Portable ZIP built: ${SCRIPT_DIR}/build-container/ModOrganizer-linux-x86_64.zip" diff --git a/docker/AppRun.sh b/docker/AppRun.sh new file mode 100644 index 0000000..f0ae807 --- /dev/null +++ b/docker/AppRun.sh @@ -0,0 +1,125 @@ +#!/bin/bash +HERE="$(dirname "$(readlink -f "$0")")" +APPIMAGE_DIR="$(dirname "$(readlink -f "${APPIMAGE:-$0}")")" + +cleanup_stale_fuse_mounts() { + local root="$1" + [ -n "${root}" ] || return 0 + [ -r /proc/mounts ] || return 0 + + local src mp fstype rest mount_path stat_out + while IFS=' ' read -r src mp fstype rest; do + # Decode escaped paths from /proc/mounts. + mount_path="${mp//\\040/ }" + mount_path="${mount_path//\\011/$'\t'}" + mount_path="${mount_path//\\012/$'\n'}" + + case "${mount_path}" in + "${root}"/*) ;; + *) continue ;; + esac + + case "${fstype}" in + fuse*|fuse.*) ;; + *) continue ;; + esac + + stat_out="$(LC_ALL=C stat "${mount_path}" 2>&1 || true)" + if printf "%s" "${stat_out}" | grep -Eq "Transport endpoint is not connected|Stale file handle|Input/output error"; then + echo "[Fluorine] Recovering stale FUSE mount: ${mount_path}" + fusermount3 -uz "${mount_path}" >/dev/null 2>&1 || \ + fusermount -uz "${mount_path}" >/dev/null 2>&1 || \ + umount -l "${mount_path}" >/dev/null 2>&1 || true + fi + done < /proc/mounts +} + +cleanup_stale_fuse_mounts "${APPIMAGE_DIR}" + +sync_dir_overwrite_files() { + local name="$1" + local src="${HERE}/usr/share/fluorine/${name}" + local dst="${APPIMAGE_DIR}/${name}" + + [ -d "${src}" ] || return 0 + + if [ ! -d "${dst}" ]; then + echo "[Fluorine] First run: extracting ${name}/ next to AppImage..." + else + echo "[Fluorine] Updating ${name}/ bundled files..." + fi + mkdir -p "${dst}" + # Merge bundled payload while skipping VCS metadata (.git, etc.). + ( + cd "${src}" + tar --exclude-vcs -cf - . + ) | ( + cd "${dst}" + tar -xf - + ) +} + +# ── Extract/sync writable dirs ── +sync_dir_overwrite_files "plugins" +sync_dir_overwrite_files "dlls" +sync_dir_overwrite_files "python" + +# Some existing Windows portable setups include plugins/plugin_python with +# .pyd/.dll payload. On Linux, the proxy will prefer this folder if present, +# which can hide the correct Linux mobase module. Overlay Linux runtime files +# into that folder to keep compatibility without deleting user content. +if [ -d "${APPIMAGE_DIR}/plugins/plugin_python" ]; then + if find "${APPIMAGE_DIR}/plugins/plugin_python" -type f \( -name '*.pyd' -o -name 'python*.dll' \) | grep -q .; then + echo "[Fluorine] Detected Windows plugin_python payload, overlaying Linux runtime files..." + mkdir -p "${APPIMAGE_DIR}/plugins/plugin_python/libs" "${APPIMAGE_DIR}/plugins/plugin_python/dlls" + [ -d "${APPIMAGE_DIR}/plugins/libs" ] && cp -a "${APPIMAGE_DIR}/plugins/libs/." "${APPIMAGE_DIR}/plugins/plugin_python/libs/" + [ -d "${APPIMAGE_DIR}/plugins/dlls" ] && cp -a "${APPIMAGE_DIR}/plugins/dlls/." "${APPIMAGE_DIR}/plugins/plugin_python/dlls/" + PYTHON_ROOT_CANDIDATE="${HERE}/usr/share/fluorine/python" + [ -d "${PYTHON_ROOT_CANDIDATE}/lib" ] || PYTHON_ROOT_CANDIDATE="${APPIMAGE_DIR}/python" + if [ -d "${PYTHON_ROOT_CANDIDATE}/lib" ]; then + PYVER_DIR="$(find "${PYTHON_ROOT_CANDIDATE}/lib" -mindepth 1 -maxdepth 1 -type d -name 'python3.*' | head -n 1)" + if [ -n "${PYVER_DIR}" ]; then + SITE_DIR="${PYVER_DIR}/site-packages" + [ -d "${SITE_DIR}/PyQt6" ] && cp -a "${SITE_DIR}/PyQt6" "${APPIMAGE_DIR}/plugins/plugin_python/libs/" + [ -d "${SITE_DIR}/PyQt6_sip" ] && cp -a "${SITE_DIR}/PyQt6_sip" "${APPIMAGE_DIR}/plugins/plugin_python/libs/" + [ -d "${SITE_DIR}/sip" ] && cp -a "${SITE_DIR}/sip" "${APPIMAGE_DIR}/plugins/plugin_python/libs/" + fi + fi + fi +fi + +# ── Environment ── +export PATH="${HERE}/usr/bin:${HERE}/usr/libexec:${PATH}" +export LD_LIBRARY_PATH="${HERE}/usr/lib:${HERE}/usr/libexec:${LD_LIBRARY_PATH:-}" + +# Qt plugins (read-only, inside AppImage) +export QT_PLUGIN_PATH="${HERE}/usr/plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="${HERE}/usr/plugins/platforms" +if [ -x "${HERE}/usr/libexec/QtWebEngineProcess" ]; then + export QTWEBENGINEPROCESS_PATH="${HERE}/usr/libexec/QtWebEngineProcess" +fi +if [ -d "${HERE}/usr/resources" ]; then + export QTWEBENGINE_RESOURCES_PATH="${HERE}/usr/resources" +fi +if [ -d "${HERE}/usr/translations/qtwebengine_locales" ]; then + export QTWEBENGINE_LOCALES_PATH="${HERE}/usr/translations/qtwebengine_locales" +fi + +# Tell the app to use the writable dirs next to the AppImage. +# MO2_BASE_DIR overrides qApp->applicationDirPath() for plugin/dll discovery. +export MO2_BASE_DIR="${APPIMAGE_DIR}" +export MO2_PLUGINS_DIR="${APPIMAGE_DIR}/plugins" +export MO2_DLLS_DIR="${APPIMAGE_DIR}/dlls" +MO2_PYTHON_BUNDLED="${HERE}/usr/share/fluorine/python" +if [ -d "${MO2_PYTHON_BUNDLED}/lib" ]; then + export MO2_PYTHON_DIR="${MO2_PYTHON_BUNDLED}" +else + export MO2_PYTHON_DIR="${APPIMAGE_DIR}/python" +fi +# Do not export PYTHONHOME/PYTHONPATH globally here. MO2 sets Python runtime +# internally for plugin_python, while child processes (umu/NaK/launchers) must +# use their own system Python environment. +unset PYTHONHOME PYTHONPATH PYTHONNOUSERSITE + +cd "${APPIMAGE_DIR}" +exec "${HERE}/usr/bin/ModOrganizer.bin" "$@" diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..fc62dc8 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,98 @@ +FROM ubuntu:25.10 + +ENV DEBIAN_FRONTEND=noninteractive +ENV RUSTUP_HOME=/opt/rust/rustup +ENV CARGO_HOME=/opt/rust/cargo +ENV PATH="/opt/rust/cargo/bin:${PATH}" + +# ── System build tools + all library dependencies ── +RUN apt-get update && apt-get install -y --no-install-recommends \ + # Build essentials + build-essential cmake ninja-build ccache \ + git curl wget file binutils patchelf pkg-config ca-certificates \ + # Library dependencies + libboost-all-dev \ + libsqlite3-dev \ + libtinyxml2-dev \ + libfontconfig1-dev \ + libspdlog-dev \ + libfuse3-dev fuse3 \ + liblz4-dev zlib1g-dev libzstd-dev libbz2-dev liblzma-dev \ + libssl-dev libcurl4-openssl-dev \ + libtomlplusplus-dev \ + # Qt 6 + qt6-base-dev qt6-base-dev-tools \ + qt6-webengine-dev \ + libqt6websockets6-dev \ + qt6-wayland \ + # Python + python3 python3-dev python3-pip python3-venv \ + pybind11-dev \ + python3-pyqt6 \ + # Misc + icoutils zip unzip \ + && rm -rf /var/lib/apt/lists/* + +# ── Python SIP tooling + runtime deps ── +RUN pip3 install --break-system-packages sip psutil vdf || true + +# ── Rust toolchain ── +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ + sh -s -- -y --default-toolchain stable --profile minimal + +# ── Portable Python runtime (for bundling, not compilation) ── +ARG PORTABLE_PYTHON_VERSION=3.13.9 +RUN curl -L --retry 3 -o /tmp/portable-python.zip \ + "https://github.com/bjia56/portable-python/releases/download/cpython-v${PORTABLE_PYTHON_VERSION}-build.0/python-headless-${PORTABLE_PYTHON_VERSION}-linux-x86_64.zip" && \ + mkdir -p /tmp/pp-extract && \ + unzip -q /tmp/portable-python.zip -d /tmp/pp-extract && \ + mv /tmp/pp-extract/python-headless-*/ /opt/portable-python && \ + rm -rf /tmp/portable-python.zip /tmp/pp-extract + +# ── Build and install libloot ── +ARG LIBLOOT_REF=master +RUN git clone --depth 1 --branch ${LIBLOOT_REF} \ + https://github.com/loot/libloot.git /tmp/libloot && \ + cmake -S /tmp/libloot/cpp -B /tmp/libloot/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=ON \ + -DLIBLOOT_INSTALL_DOCS=OFF \ + -DBUILD_TESTING=OFF \ + -DCMAKE_INSTALL_PREFIX=/usr/local && \ + cmake --build /tmp/libloot/build --parallel && \ + cmake --install /tmp/libloot/build && \ + # Create pkg-config metadata + mkdir -p /usr/local/lib/pkgconfig && \ + printf '%s\n' \ + 'prefix=/usr/local' \ + 'exec_prefix=${prefix}' \ + 'libdir=${prefix}/lib' \ + 'includedir=${prefix}/include' \ + '' \ + 'Name: libloot' \ + 'Description: LOOT C++ API library' \ + 'Version: 0.29.0' \ + 'Libs: -L${libdir} -lloot' \ + 'Cflags: -I${includedir}' \ + > /usr/local/lib/pkgconfig/libloot.pc && \ + ldconfig && \ + rm -rf /tmp/libloot + +# ── Pre-download linuxdeploy tooling ── +RUN mkdir -p /opt/linuxdeploy && \ + curl -L --retry 3 -o /opt/linuxdeploy/linuxdeploy-x86_64.AppImage \ + https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage && \ + curl -L --retry 3 -o /opt/linuxdeploy/linuxdeploy-plugin-qt-x86_64.AppImage \ + https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage && \ + chmod +x /opt/linuxdeploy/linuxdeploy-x86_64.AppImage \ + /opt/linuxdeploy/linuxdeploy-plugin-qt-x86_64.AppImage && \ + # Extract Qt plugin (avoids FUSE requirement at runtime) + cd /opt/linuxdeploy && \ + ./linuxdeploy-plugin-qt-x86_64.AppImage --appimage-extract >/dev/null && \ + mv squashfs-root linuxdeploy-plugin-qt.AppDir && \ + chmod +x linuxdeploy-plugin-qt.AppDir/AppRun && \ + ln -sf /opt/linuxdeploy/linuxdeploy-plugin-qt.AppDir/AppRun \ + /opt/linuxdeploy/linuxdeploy-plugin-qt && \ + chmod -x /opt/linuxdeploy/linuxdeploy-plugin-qt-x86_64.AppImage + +WORKDIR /build diff --git a/docker/build-inner.sh b/docker/build-inner.sh new file mode 100755 index 0000000..efbb3d7 --- /dev/null +++ b/docker/build-inner.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ── Build ── +cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DPython3_EXECUTABLE="$(command -v python3)" \ + -DBUILD_PLUGIN_PYTHON=ON + +cmake --build build --parallel + +MODORG_BIN="build/src/src/ModOrganizer" +if [ ! -f "${MODORG_BIN}" ]; then + echo "ERROR: ModOrganizer binary not found at ${MODORG_BIN}" + exit 1 +fi +RUNDIR="build/src/src" + +PY_MM="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" + +# ── Output layout ── +OUT_DIR="/src/build/ModOrganizer-portable" +ZIP_OUT="/src/build/ModOrganizer-linux-x86_64.zip" +rm -rf "${OUT_DIR}" "${ZIP_OUT}" +mkdir -p "${OUT_DIR}/plugins" "${OUT_DIR}/dlls" "${OUT_DIR}/lib" + +# ── Main binary + helpers ── +cp -f "${RUNDIR}/ModOrganizer" "${OUT_DIR}/ModOrganizer-core" +if [ -f "${RUNDIR}/umu-run" ]; then + # Patch umu-run to fix two issues with Steamworks DRM: + # + # 1. Preserve STEAM_COMPAT_CLIENT_INSTALL_PATH from the parent environment. + # Upstream umu-run initialises this to "" and never picks up the caller's + # value, which prevents the Steam client libraries from being found. + # + # 2. Remove UMU_ID from the environment before Proton runs. GE-Proton + # treats games with UMU_ID as non-Steam titles and skips the steam.exe + # DRM bridge, causing "Application load error 5:0000065434". For actual + # Steam games (SteamAppId != "0") we delete UMU_ID so Proton uses the + # correct steam.exe launch path. + UMU_PATCH_DIR="$(mktemp -d)" + (cd "${UMU_PATCH_DIR}" && python3 << PATCHEOF +import zipfile, pathlib +zf = zipfile.ZipFile('/src/${RUNDIR}/umu-run') +zf.extractall('src') +run_py = pathlib.Path('src/umu/umu_run.py') +src = run_py.read_text() + +# Patch 1: preserve STEAM_COMPAT_CLIENT_INSTALL_PATH +old1 = ' env["STEAM_COMPAT_INSTALL_PATH"] = os.environ.get("STEAM_COMPAT_INSTALL_PATH", "")' +new1 = (old1 + + '\n env["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = os.environ.get(' + + '\n "STEAM_COMPAT_CLIENT_INSTALL_PATH", ""' + + '\n )') +if old1 in src and 'STEAM_COMPAT_CLIENT_INSTALL_PATH"] = os.environ' not in src: + src = src.replace(old1, new1) + +# Patch 2: delete UMU_ID before Proton launch for Steam games +old2 = ' os.environ[key] = val' +new2 = (old2 + + '\n\n # GE-Proton treats games with UMU_ID as non-Steam titles and skips' + + '\n # the steam.exe DRM bridge. Remove it for Steam games.' + + '\n if "UMU_ID" in os.environ and env.get("SteamAppId", "0") != "0":' + + '\n del os.environ["UMU_ID"]') +if old2 in src and 'del os.environ["UMU_ID"]' not in src: + src = src.replace(old2, new2, 1) + +run_py.write_text(src) +PATCHEOF +) + python3 -m zipapp "${UMU_PATCH_DIR}/src" -o "${OUT_DIR}/umu-run" -p '/usr/bin/env python3' + chmod +x "${OUT_DIR}/umu-run" + rm -rf "${UMU_PATCH_DIR}" +fi +[ -f "${RUNDIR}/README-PORTABLE.txt" ] && cp -f "${RUNDIR}/README-PORTABLE.txt" "${OUT_DIR}/" +[ -f "/src/src/fluorine-manager" ] && cp -f "/src/src/fluorine-manager" "${OUT_DIR}/" + +# lootcli (spawned by MO2 for load-order sorting). +LOOTCLI="build/libs/lootcli/src/lootcli" +[ -f "${LOOTCLI}" ] && cp -f "${LOOTCLI}" "${OUT_DIR}/" + +for tool in wrestool icotool; do + command -v "${tool}" >/dev/null 2>&1 && cp -f "$(command -v "${tool}")" "${OUT_DIR}/" +done + +# ── MO2 plugins (.so) ── +find build/libs -type f \( \ + -name "libgame_*.so" -o \ + -name "libinstaller_*.so" -o \ + -name "libpreview_*.so" -o \ + -name "libdiagnose_*.so" -o \ + -name "libcheck_*.so" -o \ + -name "libtool_*.so" -o \ + -name "libinieditor.so" -o \ + -name "libinibakery.so" -o \ + -name "libbsa_extractor.so" -o \ + -name "libbsa_packer.so" -o \ + -name "libproxy.so" \ +\) -exec cp -f {} "${OUT_DIR}/plugins/" \; + +# Python plugin payload. +for f in libplugin_python.so lzokay.py winreg.py pyCfg.py \ + DDSPreview.py Form43Checker.py ScriptExtenderPluginChecker.py; do + [ -f "build/src/src/plugins/${f}" ] && cp -f "build/src/src/plugins/${f}" "${OUT_DIR}/plugins/" +done +for d in basic_games data libs dlls; do + [ -d "build/src/src/plugins/${d}" ] && cp -a "build/src/src/plugins/${d}" "${OUT_DIR}/plugins/" +done +rm -f "${OUT_DIR}/plugins/FNIS"*.py + +# ── 7z runtime ── +SO7="build/src/src/dlls/7z.so" +if [ -f "${SO7}" ]; then + cp -f "${SO7}" "${OUT_DIR}/dlls/7z.so" + cp -f "${SO7}" "${OUT_DIR}/dlls/7zip.dll" +fi + +# ── Project-specific shared libraries ── +cp -f build/libs/uibase/src/libuibase.so "${OUT_DIR}/lib/" +cp -f build/libs/libbsarch/liblibbsarch.so "${OUT_DIR}/lib/" +cp -f build/libs/archive/src/libarchive.so "${OUT_DIR}/lib/" +cp -f build/libs/plugin_python/src/runner/librunner.so "${OUT_DIR}/lib/" +for ffi in libs/bsa_ffi/target/release/libbsa_ffi.so \ + libs/nak_ffi/target/release/libnak_ffi.so; do + [ -f "${ffi}" ] && cp -f "${ffi}" "${OUT_DIR}/lib/" +done + +# Boost (version-pinned to container, won't exist on most user systems). +for boost_lib in /lib/x86_64-linux-gnu/libboost_program_options.so* \ + /lib/x86_64-linux-gnu/libboost_thread.so*; do + [ -f "${boost_lib}" ] && cp -Lf "${boost_lib}" "${OUT_DIR}/lib/" +done + +# libloot (custom-built, never on user systems). +if [ -f /usr/local/lib/libloot.so.0 ]; then + cp -Lf /usr/local/lib/libloot.so.0 "${OUT_DIR}/lib/" + # Create the unversioned symlink too. + ln -sf libloot.so.0 "${OUT_DIR}/lib/libloot.so" +fi + +# ── Portable Python runtime ── +PORTABLE_PY="/opt/portable-python" +if [ -d "${PORTABLE_PY}" ]; then + echo "Bundling portable Python from ${PORTABLE_PY}..." + cp -a "${PORTABLE_PY}" "${OUT_DIR}/python" + + # Trim unnecessary files from portable Python. + PP_STDLIB="${OUT_DIR}/python/lib/python${PY_MM}" + rm -rf "${PP_STDLIB}/test" \ + "${PP_STDLIB}/unittest/test" \ + "${PP_STDLIB}/idlelib" \ + "${PP_STDLIB}/tkinter" \ + "${PP_STDLIB}/turtledemo" \ + "${PP_STDLIB}/__pycache__" \ + "${OUT_DIR}/python/include" \ + "${OUT_DIR}/python/share" \ + 2>/dev/null || true + find "${OUT_DIR}/python" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + find "${OUT_DIR}/python" -name "*.pyc" -delete 2>/dev/null || true + + # Ensure versioned soname symlink exists (pybind11 links against libpython3.13.so.1.0). + if [ -f "${OUT_DIR}/python/lib/libpython${PY_MM}.so" ] && \ + [ ! -f "${OUT_DIR}/python/lib/libpython${PY_MM}.so.1.0" ]; then + ln -sf "libpython${PY_MM}.so" "${OUT_DIR}/python/lib/libpython${PY_MM}.so.1.0" + fi +else + echo "ERROR: Portable Python not found at ${PORTABLE_PY}" + exit 1 +fi + +# Bundle PyQt6 from system into portable Python's site-packages. +PYSITE="${OUT_DIR}/python/lib/python${PY_MM}/site-packages" +mkdir -p "${PYSITE}" +for search_dir in /usr/lib/python3/dist-packages \ + "/usr/lib/python${PY_MM}/dist-packages" \ + "/usr/local/lib/python${PY_MM}/dist-packages"; do + if [ -d "${search_dir}/PyQt6" ]; then + echo "Bundling PyQt6 from ${search_dir}..." + cp -a "${search_dir}/PyQt6" "${PYSITE}/" + [ -d "${search_dir}/PyQt6_sip" ] && cp -a "${search_dir}/PyQt6_sip" "${PYSITE}/" + [ -d "${search_dir}/sip" ] && cp -a "${search_dir}/sip" "${PYSITE}/" + break + fi +done + +# Bundle pip-installed Python packages (psutil etc.). +for search_dir in "/usr/local/lib/python${PY_MM}/dist-packages" \ + /usr/lib/python3/dist-packages \ + "/usr/lib/python${PY_MM}/dist-packages"; do + for pkg in psutil vdf; do + [ -d "${search_dir}/${pkg}" ] && [ ! -d "${PYSITE}/${pkg}" ] && \ + cp -a "${search_dir}/${pkg}" "${PYSITE}/" + done +done + +# Build-tree Python plugin payload. +[ -d build/src/src/python ] && cp -a build/src/src/python/. "${OUT_DIR}/python/" + +# ── Strip all MO2 binaries (not portable Python) ── +echo "Stripping MO2 binaries..." +strip --strip-unneeded "${OUT_DIR}/ModOrganizer-core" 2>/dev/null || true +find "${OUT_DIR}/plugins" -name "*.so" -exec strip --strip-unneeded {} \; 2>/dev/null || true +find "${OUT_DIR}/dlls" -name "*.so" -o -name "*.dll" | xargs -r strip --strip-unneeded 2>/dev/null || true +find "${OUT_DIR}/lib" -name "*.so" -exec strip --strip-unneeded {} \; 2>/dev/null || true +for tool in wrestool icotool lootcli; do + [ -f "${OUT_DIR}/${tool}" ] && strip --strip-unneeded "${OUT_DIR}/${tool}" 2>/dev/null || true +done + +# ── Validate embedded Python runtime ── +cat > /tmp/mo2_embed_py_check.c <<'C' +#include +int main(void) { + Py_Initialize(); + int rc = PyRun_SimpleString( + "import zlib\n" + "import runpy\n" + "import zipimport\n" + "print('python embed check ok')\n"); + if (PyErr_Occurred()) { + PyErr_Print(); + } + Py_Finalize(); + return rc; +} +C +gcc /tmp/mo2_embed_py_check.c -o /tmp/mo2_embed_py_check $(python3-config --embed --cflags --ldflags) +if ! PYTHONHOME="${OUT_DIR}/python" \ + PYTHONPATH="${OUT_DIR}/python/lib/python${PY_MM}:${PYSITE}" \ + LD_LIBRARY_PATH="${OUT_DIR}/lib:${OUT_DIR}/python/lib:${LD_LIBRARY_PATH:-}" \ + /tmp/mo2_embed_py_check; then + echo "ERROR: Embedded Python runtime check failed." + exit 1 +fi + +# ── Launcher script ── +cat > "${OUT_DIR}/ModOrganizer" <<'LAUNCH' +#!/usr/bin/env bash +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +export PATH="${HERE}:${PATH}" +export LD_LIBRARY_PATH="${HERE}/lib:${HERE}/python/lib:${LD_LIBRARY_PATH:-}" +export MO2_BASE_DIR="${HERE}" +export MO2_PLUGINS_DIR="${HERE}/plugins" +export MO2_DLLS_DIR="${HERE}/dlls" +export MO2_PYTHON_DIR="${HERE}/python" +# PYTHONHOME is set only for the MO2 process (not exported to children like +# umu-run/Proton which have their own Python). MO2_PYTHON_DIR lets the +# binary reconstruct it internally. +MO2_PYTHONHOME="${HERE}/python" +unset PYTHONPATH PYTHONNOUSERSITE PYTHONHOME + +# Find system Qt6 plugins (compiled-in path won't match across distros). +if [ -z "${QT_PLUGIN_PATH:-}" ]; then + for qt_dir in /usr/lib/qt6/plugins \ + /usr/lib/x86_64-linux-gnu/qt6/plugins \ + /usr/lib64/qt6/plugins; do + if [ -d "${qt_dir}/platforms" ]; then + export QT_PLUGIN_PATH="${qt_dir}" + break + fi + done + if [ -z "${QT_PLUGIN_PATH:-}" ] && command -v qtpaths6 >/dev/null 2>&1; then + export QT_PLUGIN_PATH="$(qtpaths6 --plugin-dir)" + fi +fi + +# Quick dependency check. +missing="$(ldd "${HERE}/ModOrganizer-core" 2>/dev/null | grep "not found" || true)" +if [ -n "${missing}" ]; then + echo "ERROR: Missing system libraries:" + echo "${missing}" + echo "" + echo "On Arch/CachyOS: sudo pacman -S qt6-base qt6-websockets qt6-wayland" + echo "On Fedora: sudo dnf install qt6-qtbase qt6-qtwebsockets qt6-qtwayland" + echo "On Ubuntu/Debian: sudo apt install qt6-base-dev libqt6websockets6-dev qt6-wayland" + exit 1 +fi + +cd "${HERE}" +exec env PYTHONHOME="${MO2_PYTHONHOME}" "${HERE}/ModOrganizer-core" "$@" +LAUNCH +chmod +x "${OUT_DIR}/ModOrganizer" + +# ── ZIP ── +( + cd "${OUT_DIR}" + zip -r -9 "${ZIP_OUT}" . +) + +# ── Summary ── +echo "" +echo "=== Package Summary ===" +du -sh "${OUT_DIR}"/*/ "${OUT_DIR}"/ModOrganizer-core 2>/dev/null | sort -rh +echo "" +ZIP_SIZE="$(du -sh "${ZIP_OUT}" | cut -f1)" +echo "Done! Portable ZIP at: build/ModOrganizer-linux-x86_64.zip (${ZIP_SIZE})" diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000..e47f5b5 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,26 @@ +# FAQ + +## Where Are Logs Stored? +Logs are written next to the app binary in a `logs/` folder. + +## Does Removing an Instance Delete My Files? +Not by default. It removes the profile from the menu, and gives you the option to delete it if you want to. + +## Do I Need UMU and 7ZZ? +Yes, both are bundled in release artifacts under `bin/`. + +## Do I Need to Install 9 Million Different Dependencies? +No, the dependencies are handled by NaK! If there is something missing I will gladly add it to the list. This also includes WINEDLLOVERWRITES as well! + +## Make Sure to Select a Proton in the Settings Before Playing! +You will need to create a prefix before being able to play, I have added a Wine/Proton tab in the settings. Just let it install all the dependencies and then you are good to play. +Screenshot_20260211_021905 + +## Does It Work with Existing Modlists? +Yes, it can phrase wine paths and read them out as Linux paths in the GUI. It will also save the paths as wine paths in case you move to MO2 via proton/wine. + +To use a portable install you can run this as an example. `flatpak run com.fluorine.manager --instance /home/luke/Games/Skyrim/` and it should pick right up where you left off. + +And all the buttons like associate with mod manager downloads button and MO2 OAuth also works. + +FAQ is going to be updated with more info in the future. diff --git a/flatpak/com.fluorine.manager.desktop b/flatpak/com.fluorine.manager.desktop new file mode 100644 index 0000000..ff62c76 --- /dev/null +++ b/flatpak/com.fluorine.manager.desktop @@ -0,0 +1,22 @@ +[Desktop Entry] +Type=Application +Name=Fluorine Manager +GenericName=Mod Manager +Comment=Mod Organizer 2 for Linux - manage your game mods +Exec=fluorine-manager +Icon=com.fluorine.manager +Terminal=false +Categories=Game;Utility; +Keywords=mod;organizer;modding;skyrim;fallout; +MimeType=x-scheme-handler/nxm; +Actions=create-portable;list-instances; + +[Desktop Action create-portable] +Name=Create Portable Instance +Exec=fluorine-manager create-portable +Icon=com.fluorine.manager + +[Desktop Action list-instances] +Name=List Instances +Exec=fluorine-manager list-instances +Icon=com.fluorine.manager diff --git a/flatpak/com.fluorine.manager.metainfo.xml b/flatpak/com.fluorine.manager.metainfo.xml new file mode 100644 index 0000000..1200d3e --- /dev/null +++ b/flatpak/com.fluorine.manager.metainfo.xml @@ -0,0 +1,38 @@ + + + com.fluorine.manager + Fluorine Manager + Mod Organizer 2 for Linux + CC0-1.0 + GPL-3.0-or-later + + +

+ Fluorine Manager is a Linux port of Mod Organizer 2, a powerful mod management + tool for Bethesda games and other titles. It uses a virtual filesystem to keep + your game directory clean while allowing complex mod setups with load order + management, conflict resolution, and profile support. +

+

Features:

+
    +
  • Virtual filesystem - mods never touch your game folder
  • +
  • Profile system for different mod configurations
  • +
  • Plugin load order management
  • +
  • Mod conflict detection and resolution
  • +
  • Nexus Mods integration for downloading
  • +
  • Proton/Wine prefix management for running Windows games
  • +
  • UMU launcher support
  • +
+
+ + com.fluorine.manager.desktop + + https://github.com/ModOrganizer2/modorganizer + https://github.com/ModOrganizer2/modorganizer/issues + + + fluorine-manager + + + +
diff --git a/flatpak/com.fluorine.manager.png b/flatpak/com.fluorine.manager.png new file mode 100644 index 0000000..3a9fbdd Binary files /dev/null and b/flatpak/com.fluorine.manager.png differ diff --git a/flatpak/com.fluorine.manager.yml b/flatpak/com.fluorine.manager.yml new file mode 100644 index 0000000..a0e2122 --- /dev/null +++ b/flatpak/com.fluorine.manager.yml @@ -0,0 +1,364 @@ +app-id: com.fluorine.manager +runtime: org.kde.Platform +runtime-version: '6.10' +sdk: org.kde.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.rust-stable +command: fluorine-manager + +finish-args: + - --share=ipc + - --share=network + - --socket=x11 + - --socket=wayland + - --device=all + - --filesystem=home + - --filesystem=/run/media:ro + - --filesystem=~/.steam:ro + - --filesystem=~/.local/share/Steam + - --filesystem=~/.var/app/com.valvesoftware.Steam:ro + - --talk-name=org.freedesktop.Flatpak + +cleanup: + - /include + - /lib/cmake + - /lib/pkgconfig + - /share/doc + - /share/man + - '*.a' + - '*.la' + +modules: + # ── 1. Boost ── + - name: boost + buildsystem: simple + build-commands: + - ./bootstrap.sh --prefix=/app --with-libraries=program_options,thread,filesystem,system,locale + - ./b2 install -j$FLATPAK_BUILDER_N_JOBS link=shared variant=release + sources: + - type: archive + url: https://archives.boost.io/release/1.87.0/source/boost_1_87_0.tar.gz + sha256: f55c340aa49763b1925ccf02b2e83f35fdcf634c9d5164a2acb87540173c741d + + # ── 2. spdlog ── + - name: spdlog + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=Release + - -DSPDLOG_BUILD_SHARED=ON + sources: + - type: git + url: https://github.com/gabime/spdlog.git + tag: v1.15.2 + + # ── 3. tinyxml2 ── + - name: tinyxml2 + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=Release + sources: + - type: git + url: https://github.com/leethomason/tinyxml2.git + tag: 10.0.0 + + # ── 4. tomlplusplus ── + - name: tomlplusplus + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=Release + sources: + - type: git + url: https://github.com/marzer/tomlplusplus.git + tag: v3.4.0 + + # ── 5. libfuse3 ── + # Built manually to install fusermount3 without setuid (chown root fails in sandbox). + # default_library=both produces libfuse3.a so the VFS helper can link statically. + - name: libfuse3 + buildsystem: simple + build-commands: + - meson setup _build --prefix=/app -Dexamples=false -Dtests=false -Dutils=true -Ddefault_library=both + - ninja -C _build + # Install library and headers (skip meson install which runs chown). + - ninja -C _build install || true + # Ensure fusermount3 is installed even if install_helper.sh failed. + - install -Dm755 _build/util/fusermount3 /app/bin/fusermount3 2>/dev/null || true + sources: + - type: git + url: https://github.com/libfuse/libfuse.git + tag: fuse-3.16.2 + + # ── 6. pybind11 ── + - name: pybind11 + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=Release + - -DPYBIND11_TEST=OFF + sources: + - type: git + url: https://github.com/pybind/pybind11.git + tag: v2.13.6 + + # ── 7. libloot ── + # libloot has a Rust crate at the root and a C++ cmake project in cpp/. + # The Rust static library must be built first, then cmake links against it. + - name: libloot + buildsystem: simple + build-options: + append-path: /usr/lib/sdk/rust-stable/bin + env: + CARGO_HOME: /run/build/libloot/cargo + # Network needed for cargo deps and cmake FetchContent (googletest). + build-args: + - --share=network + build-commands: + # Build Rust static library first. + - cargo build --release + # Build the C++ shared library (links against Rust static lib). + - | + cmake -S cpp -B cpp/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=ON \ + -DLIBLOOT_INSTALL_DOCS=OFF \ + -DCMAKE_INSTALL_PREFIX=/app + - cmake --build cpp/build --parallel + - cmake --install cpp/build + # Create pkg-config metadata. + - mkdir -p /app/lib/pkgconfig + - | + printf '%s\n' \ + 'prefix=/app' \ + 'exec_prefix=${prefix}' \ + 'libdir=${prefix}/lib' \ + 'includedir=${prefix}/include' \ + '' \ + 'Name: libloot' \ + 'Description: LOOT C++ API library' \ + 'Version: 0.29.0' \ + 'Libs: -L${libdir} -lloot' \ + 'Cflags: -I${includedir}' \ + > /app/lib/pkgconfig/libloot.pc + sources: + - type: git + url: https://github.com/loot/libloot.git + branch: master + + # ── 8. icoutils ── + - name: icoutils + buildsystem: autotools + build-options: + # icoutils 0.32.3 uses K&R-style () declarations that GCC 14+ rejects. + cflags: -std=gnu17 + sources: + - type: archive + url: https://download.savannah.gnu.org/releases/icoutils/icoutils-0.32.3.tar.bz2 + sha256: 17abe02d043a253b68b47e3af69c9fc755b895db68fdc8811786125df564c6e0 + + # ── 9. cabextract ── + # Required by winetricks for extracting Windows cab archives. + - name: cabextract + buildsystem: autotools + sources: + - type: archive + url: https://www.cabextract.org.uk/cabextract-1.11.tar.gz + sha256: b5546db1155e4c718ff3d4b278573604f30dd64c3c5bfd4657cd089b823a3ac6 + + # ── 10. Portable Python runtime ── + # Pre-built Python used as the embedded interpreter for MO2's Python plugins. + # pip packages are installed via network during build (--share=network). + - name: portable-python + buildsystem: simple + build-options: + build-args: + - --share=network + build-commands: + # Extract, trim, symlink, and install pip packages in one step + # (each build-command runs in its own shell). + - | + set -e + mkdir -p /app/lib/fluorine/python + unzip portable-python.zip -d /tmp/pp + PP_DIR="$(find /tmp/pp -maxdepth 1 -mindepth 1 -type d | head -1)" + cp -a "${PP_DIR}/." /app/lib/fluorine/python/ + rm -rf /tmp/pp + # Versioned soname symlink (pybind11 links against libpython3.13.so.1.0). + if [ -f /app/lib/fluorine/python/lib/libpython3.13.so ] && \ + [ ! -f /app/lib/fluorine/python/lib/libpython3.13.so.1.0 ]; then + ln -sf libpython3.13.so /app/lib/fluorine/python/lib/libpython3.13.so.1.0 + fi + # Trim test suites and unnecessary files. + rm -rf /app/lib/fluorine/python/lib/python3.13/test \ + /app/lib/fluorine/python/lib/python3.13/unittest/test \ + /app/lib/fluorine/python/lib/python3.13/idlelib \ + /app/lib/fluorine/python/lib/python3.13/tkinter \ + /app/lib/fluorine/python/lib/python3.13/turtledemo \ + /app/lib/fluorine/python/include \ + /app/lib/fluorine/python/share \ + 2>/dev/null || true + find /app/lib/fluorine/python -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + find /app/lib/fluorine/python -name "*.pyc" -delete 2>/dev/null || true + # Install pip packages (PyQt6 for plugin UI, psutil, vdf). + /app/lib/fluorine/python/bin/python3 -m ensurepip --default-pip 2>/dev/null || true + /app/lib/fluorine/python/bin/python3 -m pip install --no-cache-dir PyQt6 psutil vdf + sources: + - type: file + url: https://github.com/bjia56/portable-python/releases/download/cpython-v3.13.9-build.0/python-headless-3.13.9-linux-x86_64.zip + sha256: d03507d107da86d74aa38bbc4957b8db5753de567024c5724a254909ae6b86d6 + dest-filename: portable-python.zip + + # ── 10. Fluorine Manager (main project) ── + - name: fluorine + buildsystem: simple + build-options: + append-path: /usr/lib/sdk/rust-stable/bin + env: + CARGO_HOME: /run/build/fluorine/cargo + PYTHONPATH: /app/lib/python3/dist-packages + # Network needed for FetchContent (7zip, etc.) and cargo deps. + build-args: + - --share=network + build-commands: + # Install sip build tools into /app (SDK /usr is read-only). + - /usr/bin/python3 -m pip install --target=/app/lib/python3/dist-packages sip + + # ── Configure and build ── + - | + cmake -S . -B _build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH=/app \ + -DPython3_ROOT_DIR=/app/lib/fluorine/python \ + -DPython3_EXECUTABLE=/app/lib/fluorine/python/bin/python3 \ + -DBUILD_PLUGIN_PYTHON=ON + - cmake --build _build --parallel + + # ── Create output layout ── + - mkdir -p /app/lib/fluorine/plugins /app/lib/fluorine/dlls /app/lib/fluorine/lib + + # Main binary. + - cp -f _build/src/src/ModOrganizer /app/lib/fluorine/ModOrganizer-core + + # umu-run (patch and repackage if present). + - | + if [ -f _build/src/src/umu-run ]; then + UMU_PATCH_DIR="$(mktemp -d)" + cd "${UMU_PATCH_DIR}" + python3 << 'PATCHEOF' + import zipfile, pathlib + zf = zipfile.ZipFile('/run/build/fluorine/_build/src/src/umu-run') + zf.extractall('src') + run_py = pathlib.Path('src/umu/umu_run.py') + src = run_py.read_text() + old1 = ' env["STEAM_COMPAT_INSTALL_PATH"] = os.environ.get("STEAM_COMPAT_INSTALL_PATH", "")' + new1 = (old1 + + '\n env["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = os.environ.get(' + + '\n "STEAM_COMPAT_CLIENT_INSTALL_PATH", ""' + + '\n )') + if old1 in src and 'STEAM_COMPAT_CLIENT_INSTALL_PATH"] = os.environ' not in src: + src = src.replace(old1, new1) + old2 = ' os.environ[key] = val' + new2 = (old2 + + '\n\n # GE-Proton treats games with UMU_ID as non-Steam titles and skips' + + '\n # the steam.exe DRM bridge. Remove it for Steam games.' + + '\n if "UMU_ID" in os.environ and env.get("SteamAppId", "0") != "0":' + + '\n del os.environ["UMU_ID"]') + if old2 in src and 'del os.environ["UMU_ID"]' not in src: + src = src.replace(old2, new2, 1) + run_py.write_text(src) + PATCHEOF + python3 -m zipapp src -o /app/lib/fluorine/umu-run -p '/usr/bin/env python3' + chmod +x /app/lib/fluorine/umu-run + cd /run/build/fluorine + rm -rf "${UMU_PATCH_DIR}" + fi + + # VFS helper (standalone, runs on host for Flatpak FUSE support). + # Statically linked against libfuse3 — no runtime deps beyond glibc. + - test -f _build/src/src/mo2-vfs-helper && cp -f _build/src/src/mo2-vfs-helper /app/lib/fluorine/ || true + + # lootcli. + - test -f _build/libs/lootcli/src/lootcli && cp -f _build/libs/lootcli/src/lootcli /app/lib/fluorine/ || true + + # icoutils (symlink from /app/bin where the icoutils module installed them). + - ln -sf /app/bin/wrestool /app/lib/fluorine/wrestool + - ln -sf /app/bin/icotool /app/lib/fluorine/icotool + - ln -sf /app/bin/fusermount3 /app/lib/fluorine/fusermount3 + - ln -sf /app/bin/cabextract /app/lib/fluorine/cabextract + + # fluorine-manager CLI helper. + - test -f src/fluorine-manager && cp -f src/fluorine-manager /app/lib/fluorine/ || true + + # MO2 plugins (.so). + - | + find _build/libs -type f \( \ + -name "libgame_*.so" -o \ + -name "libinstaller_*.so" -o \ + -name "libpreview_*.so" -o \ + -name "libdiagnose_*.so" -o \ + -name "libcheck_*.so" -o \ + -name "libtool_*.so" -o \ + -name "libinieditor.so" -o \ + -name "libinibakery.so" -o \ + -name "libbsa_extractor.so" -o \ + -name "libbsa_packer.so" -o \ + -name "libproxy.so" \ + \) -exec cp -f {} /app/lib/fluorine/plugins/ \; + + # Python plugin payload. + - | + for f in libplugin_python.so lzokay.py winreg.py pyCfg.py \ + DDSPreview.py Form43Checker.py ScriptExtenderPluginChecker.py; do + [ -f "_build/src/src/plugins/${f}" ] && cp -f "_build/src/src/plugins/${f}" /app/lib/fluorine/plugins/ + done + for d in basic_games data libs dlls; do + [ -d "_build/src/src/plugins/${d}" ] && cp -a "_build/src/src/plugins/${d}" /app/lib/fluorine/plugins/ + done + rm -f /app/lib/fluorine/plugins/FNIS*.py + + # 7z runtime. + - | + if [ -f _build/src/src/dlls/7z.so ]; then + cp -f _build/src/src/dlls/7z.so /app/lib/fluorine/dlls/7z.so + cp -f _build/src/src/dlls/7z.so /app/lib/fluorine/dlls/7zip.dll + fi + + # Project shared libraries. + - cp -f _build/libs/uibase/src/libuibase.so /app/lib/fluorine/lib/ + - cp -f _build/libs/libbsarch/liblibbsarch.so /app/lib/fluorine/lib/ + - cp -f _build/libs/archive/src/libarchive.so /app/lib/fluorine/lib/ + - test -f _build/libs/plugin_python/src/runner/librunner.so && cp -f _build/libs/plugin_python/src/runner/librunner.so /app/lib/fluorine/lib/ || true + - test -f libs/bsa_ffi/target/release/libbsa_ffi.so && cp -f libs/bsa_ffi/target/release/libbsa_ffi.so /app/lib/fluorine/lib/ || true + - test -f libs/nak_ffi/target/release/libnak_ffi.so && cp -f libs/nak_ffi/target/release/libnak_ffi.so /app/lib/fluorine/lib/ || true + + # Boost libraries (from /app/lib where the boost module installed them). + - cp -Lf /app/lib/libboost_program_options.so* /app/lib/fluorine/lib/ 2>/dev/null || true + - cp -Lf /app/lib/libboost_thread.so* /app/lib/fluorine/lib/ 2>/dev/null || true + + # libloot (from /app/lib where the libloot module installed it). + - | + if [ -f /app/lib/libloot.so.0 ]; then + cp -Lf /app/lib/libloot.so.0 /app/lib/fluorine/lib/ + ln -sf libloot.so.0 /app/lib/fluorine/lib/libloot.so + elif [ -f /app/lib/libloot.so ]; then + cp -Lf /app/lib/libloot.so /app/lib/fluorine/lib/ + fi + + # Build-tree Python plugin payload. + - test -d _build/src/src/python && cp -a _build/src/src/python/. /app/lib/fluorine/python/ || true + + # Strip binaries. + - strip --strip-unneeded /app/lib/fluorine/ModOrganizer-core 2>/dev/null || true + - find /app/lib/fluorine/plugins -name "*.so" -exec strip --strip-unneeded {} \; 2>/dev/null || true + - find /app/lib/fluorine/dlls \( -name "*.so" -o -name "*.dll" \) -exec strip --strip-unneeded {} \; 2>/dev/null || true + - find /app/lib/fluorine/lib -name "*.so*" -exec strip --strip-unneeded {} \; 2>/dev/null || true + - test -f /app/lib/fluorine/lootcli && strip --strip-unneeded /app/lib/fluorine/lootcli 2>/dev/null || true + - test -f /app/lib/fluorine/mo2-vfs-helper && strip --strip-unneeded /app/lib/fluorine/mo2-vfs-helper 2>/dev/null || true + + # Install wrapper script, desktop file, metainfo, and icon. + - install -Dm755 flatpak/fluorine-manager-wrapper.sh /app/bin/fluorine-manager + - install -Dm644 flatpak/com.fluorine.manager.desktop /app/share/applications/com.fluorine.manager.desktop + - install -Dm644 flatpak/com.fluorine.manager.metainfo.xml /app/share/metainfo/com.fluorine.manager.metainfo.xml + - install -Dm644 flatpak/com.fluorine.manager.png /app/share/icons/hicolor/256x256/apps/com.fluorine.manager.png + + sources: + - type: dir + path: .. diff --git a/flatpak/flatpak-install.sh b/flatpak/flatpak-install.sh new file mode 100755 index 0000000..e52497e --- /dev/null +++ b/flatpak/flatpak-install.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Build the Fluorine Manager Flatpak. +# +# Usage: +# cd flatpak && bash flatpak-install.sh # build & install locally +# cd flatpak && bash flatpak-install.sh bundle # build a .flatpak file to share +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "${SCRIPT_DIR}")" +MANIFEST="${SCRIPT_DIR}/com.fluorine.manager.yml" +APP_ID="com.fluorine.manager" +BUILD_DIR="${PROJECT_DIR}/.flatpak-build" +MODE="${1:-install}" + +# ── Ensure flathub remote exists ── +if ! flatpak remote-list --user | grep -q flathub; then + echo "Adding flathub remote..." + flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo +fi + +# ── Install required Flatpak runtime and SDK ── +echo "Installing KDE Platform and SDK (may take a while on first run)..." +flatpak install --user --noninteractive flathub org.kde.Platform//6.10 org.kde.Sdk//6.10 + +echo "Installing Rust SDK extension..." +flatpak install --user --noninteractive flathub org.freedesktop.Sdk.Extension.rust-stable//25.08 + +cd "${PROJECT_DIR}" + +if [ "${MODE}" = "bundle" ]; then + # ── Build a distributable .flatpak file ── + echo "" + echo "Building Flatpak bundle (this may take a while)..." + flatpak-builder --repo="${PROJECT_DIR}/flatpak-repo" --force-clean \ + "${BUILD_DIR}" "${MANIFEST}" + flatpak build-bundle "${PROJECT_DIR}/flatpak-repo" "${PROJECT_DIR}/fluorine-manager.flatpak" "${APP_ID}" + BUNDLE_SIZE="$(du -sh fluorine-manager.flatpak | cut -f1)" + echo "" + echo "=== Bundle created: fluorine-manager.flatpak (${BUNDLE_SIZE}) ===" + echo "" + echo "Share this file with testers. They install it with:" + echo " flatpak install --user fluorine-manager.flatpak" +else + # ── Build and install locally ── + echo "" + echo "Building and installing Flatpak locally (this may take a while)..." + flatpak-builder --install --user --force-clean \ + "${BUILD_DIR}" "${MANIFEST}" + echo "" + echo "=== Flatpak installed successfully ===" +fi + +echo "" +echo "Usage:" +echo " flatpak run ${APP_ID} # launch GUI" +echo " flatpak run ${APP_ID} create-portable --name myinstance --game falloutnv" +echo " flatpak run ${APP_ID} list-instances" diff --git a/flatpak/fluorine-manager-wrapper.sh b/flatpak/fluorine-manager-wrapper.sh new file mode 100755 index 0000000..fabe535 --- /dev/null +++ b/flatpak/fluorine-manager-wrapper.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# Flatpak wrapper for Fluorine Manager (MO2 Linux) +# Sets up a writable overlay so users can add custom plugins while still +# loading the bundled ones from the read-only /app tree. +# +# For portable instances (--instance /path), the overlay is created INSIDE the +# instance directory so each portable install is fully self-contained. +# For the global instance (no --instance), a shared overlay is used. + +BUNDLED="/app/lib/fluorine" + +# ── Detect --instance argument ── +# If launching a portable instance, use its directory as the overlay target. +INSTANCE_DIR="" +PREV="" +for arg in "$@"; do + if [ "$PREV" = "--instance" ] || [ "$PREV" = "-i" ]; then + INSTANCE_DIR="$arg" + break + fi + PREV="$arg" +done + +if [ -n "$INSTANCE_DIR" ] && [ -d "$INSTANCE_DIR" ]; then + # Portable instance: overlay goes into the instance directory itself. + # Everything is self-contained: mods, profiles, plugins, dlls, libs. + USER_DIR="$INSTANCE_DIR" +else + # Global instance: shared overlay at ~/.local/share/fluorine/ + USER_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/fluorine" +fi + +# ── Create writable overlay with symlinks to bundled files ── +# This lets MO2 load bundled plugins AND any custom ones the user drops in. +# Existing files are never overwritten (user overrides take priority). +setup_overlay() { + mkdir -p "${USER_DIR}/plugins" "${USER_DIR}/dlls" "${USER_DIR}/lib" + + # Symlink bundled plugins (skip existing - user overrides take priority) + for f in "${BUNDLED}/plugins/"*; do + [ -e "$f" ] || continue + local base="$(basename "$f")" + local target="${USER_DIR}/plugins/${base}" + [ -e "$target" ] || [ -L "$target" ] || ln -sf "$f" "$target" + done + + # Symlink bundled dlls + for f in "${BUNDLED}/dlls/"*; do + [ -e "$f" ] || continue + local base="$(basename "$f")" + local target="${USER_DIR}/dlls/${base}" + [ -e "$target" ] || [ -L "$target" ] || ln -sf "$f" "$target" + done + + # Symlink bundled libs + for f in "${BUNDLED}/lib/"*; do + [ -e "$f" ] || continue + local base="$(basename "$f")" + local target="${USER_DIR}/lib/${base}" + [ -e "$target" ] || [ -L "$target" ] || ln -sf "$f" "$target" + done + + # Symlink other bundled files (binaries, tools) directly + for f in ModOrganizer-core lootcli wrestool icotool fusermount3 cabextract; do + [ -e "${BUNDLED}/$f" ] || continue + [ -e "${USER_DIR}/$f" ] || [ -L "${USER_DIR}/$f" ] || ln -sf "${BUNDLED}/$f" "${USER_DIR}/$f" + done + + # umu-run must be a real copy (not symlink to /app/) because it runs on + # the host via flatpak-spawn --host, where /app/ doesn't exist. + # Remove any stale symlink first (cp -f follows symlinks, won't replace them). + if [ -e "${BUNDLED}/umu-run" ]; then + rm -f "${USER_DIR}/umu-run" + cp "${BUNDLED}/umu-run" "${USER_DIR}/umu-run" + chmod +x "${USER_DIR}/umu-run" + fi + + # VFS helper must be a real binary copy (not a symlink to /app/) because + # it runs on the host via flatpak-spawn --host, where /app/ doesn't exist. + # libfuse3 is statically linked, so no extra .so files needed. + VFS_HELPER_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/fluorine/bin" + if [ -e "${BUNDLED}/mo2-vfs-helper" ]; then + mkdir -p "${VFS_HELPER_DIR}" + cp -f "${BUNDLED}/mo2-vfs-helper" "${VFS_HELPER_DIR}/mo2-vfs-helper" + chmod +x "${VFS_HELPER_DIR}/mo2-vfs-helper" + fi +} + +setup_overlay + +export PATH="${USER_DIR}:${BUNDLED}:${PATH}" +export LD_LIBRARY_PATH="${USER_DIR}/lib:${BUNDLED}/lib:${BUNDLED}/python/lib:${LD_LIBRARY_PATH:-}" + +# MO2 resolves plugins/dlls relative to MO2_BASE_DIR (or basePath()). +# Point it at the overlay so custom plugins are found. +export MO2_BASE_DIR="${USER_DIR}" +export MO2_PLUGINS_DIR="${USER_DIR}/plugins" +export MO2_DLLS_DIR="${USER_DIR}/dlls" +export MO2_PYTHON_DIR="${BUNDLED}/python" + +# Do NOT set PYTHONHOME globally -- it leaks into child processes (umu-run, +# Proton, winetricks) and breaks their Python. The plugin_python runner reads +# MO2_PYTHON_DIR and sets PYTHONHOME internally before loading the interpreter. +unset PYTHONHOME PYTHONPATH PYTHONNOUSERSITE + +# Qt6 plugins from KDE runtime. +export QT_PLUGIN_PATH="/usr/lib/plugins" + +exec "${BUNDLED}/ModOrganizer-core" "$@" diff --git a/libs/7zip/CMakeLists.txt b/libs/7zip/CMakeLists.txt new file mode 100644 index 0000000..915be26 --- /dev/null +++ b/libs/7zip/CMakeLists.txt @@ -0,0 +1,454 @@ +cmake_minimum_required(VERSION 3.16) + +# Build 7z.so from the official 7zip source code. +# This produces a shared library containing all archive format handlers, +# compression codecs, and crypto modules that the MO2 archive library +# loads via dlopen() at runtime. + +if(WIN32) + # On Windows, 7zip comes from vcpkg; this target is Linux-only. + return() +endif() + +include(FetchContent) +FetchContent_Declare( + 7zip_src + GIT_REPOSITORY https://github.com/ip7z/7zip.git + GIT_TAG 24.09 + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(7zip_src) + +set(SZ "${7zip_src_SOURCE_DIR}") + +# --------------------------------------------------------------------------- +# C sources (compression, hashing, crypto primitives) +# --------------------------------------------------------------------------- +set(7Z_C_SOURCES + ${SZ}/C/7zBuf2.c + ${SZ}/C/7zCrc.c + ${SZ}/C/7zCrcOpt.c + ${SZ}/C/7zStream.c + ${SZ}/C/Aes.c + ${SZ}/C/AesOpt.c + ${SZ}/C/Alloc.c + ${SZ}/C/Bcj2.c + ${SZ}/C/Bcj2Enc.c + ${SZ}/C/Blake2s.c + ${SZ}/C/Bra.c + ${SZ}/C/Bra86.c + ${SZ}/C/BraIA64.c + ${SZ}/C/BwtSort.c + ${SZ}/C/CpuArch.c + ${SZ}/C/Delta.c + ${SZ}/C/HuffEnc.c + ${SZ}/C/LzFind.c + ${SZ}/C/LzFindMt.c + ${SZ}/C/LzFindOpt.c + ${SZ}/C/Lzma2Dec.c + ${SZ}/C/Lzma2DecMt.c + ${SZ}/C/Lzma2Enc.c + ${SZ}/C/LzmaDec.c + ${SZ}/C/LzmaEnc.c + ${SZ}/C/Md5.c + ${SZ}/C/MtCoder.c + ${SZ}/C/MtDec.c + ${SZ}/C/Ppmd7.c + ${SZ}/C/Ppmd7Dec.c + ${SZ}/C/Ppmd7aDec.c + ${SZ}/C/Ppmd7Enc.c + ${SZ}/C/Ppmd8.c + ${SZ}/C/Ppmd8Dec.c + ${SZ}/C/Ppmd8Enc.c + ${SZ}/C/Sha1.c + ${SZ}/C/Sha1Opt.c + ${SZ}/C/Sha256.c + ${SZ}/C/Sha256Opt.c + ${SZ}/C/Sha3.c + ${SZ}/C/Sha512.c + ${SZ}/C/Sha512Opt.c + ${SZ}/C/Sort.c + ${SZ}/C/SwapBytes.c + ${SZ}/C/Threads.c + ${SZ}/C/Xxh64.c + ${SZ}/C/Xz.c + ${SZ}/C/XzCrc64.c + ${SZ}/C/XzCrc64Opt.c + ${SZ}/C/XzDec.c + ${SZ}/C/XzEnc.c + ${SZ}/C/XzIn.c + ${SZ}/C/ZstdDec.c +) + +# --------------------------------------------------------------------------- +# C++ Common sources +# --------------------------------------------------------------------------- +set(7Z_CPP_COMMON + ${SZ}/CPP/Common/CRC.cpp + ${SZ}/CPP/Common/CrcReg.cpp + ${SZ}/CPP/Common/DynLimBuf.cpp + ${SZ}/CPP/Common/IntToString.cpp + ${SZ}/CPP/Common/LzFindPrepare.cpp + ${SZ}/CPP/Common/Md5Reg.cpp + ${SZ}/CPP/Common/MyMap.cpp + ${SZ}/CPP/Common/MyString.cpp + ${SZ}/CPP/Common/MyVector.cpp + ${SZ}/CPP/Common/MyWindows.cpp + ${SZ}/CPP/Common/MyXml.cpp + ${SZ}/CPP/Common/NewHandler.cpp + ${SZ}/CPP/Common/Sha1Prepare.cpp + ${SZ}/CPP/Common/Sha1Reg.cpp + ${SZ}/CPP/Common/Sha256Prepare.cpp + ${SZ}/CPP/Common/Sha256Reg.cpp + ${SZ}/CPP/Common/Sha3Reg.cpp + ${SZ}/CPP/Common/Sha512Prepare.cpp + ${SZ}/CPP/Common/Sha512Reg.cpp + ${SZ}/CPP/Common/StringConvert.cpp + ${SZ}/CPP/Common/StringToInt.cpp + ${SZ}/CPP/Common/UTFConvert.cpp + ${SZ}/CPP/Common/Wildcard.cpp + ${SZ}/CPP/Common/Xxh64Reg.cpp + ${SZ}/CPP/Common/XzCrc64Init.cpp + ${SZ}/CPP/Common/XzCrc64Reg.cpp +) + +# --------------------------------------------------------------------------- +# C++ Windows compatibility layer (POSIX implementations) +# --------------------------------------------------------------------------- +set(7Z_CPP_WINDOWS + ${SZ}/CPP/Windows/FileDir.cpp + ${SZ}/CPP/Windows/FileFind.cpp + ${SZ}/CPP/Windows/FileIO.cpp + ${SZ}/CPP/Windows/FileName.cpp + ${SZ}/CPP/Windows/PropVariant.cpp + ${SZ}/CPP/Windows/PropVariantConv.cpp + ${SZ}/CPP/Windows/PropVariantUtils.cpp + ${SZ}/CPP/Windows/Synchronization.cpp + ${SZ}/CPP/Windows/System.cpp + ${SZ}/CPP/Windows/TimeUtils.cpp +) + +# --------------------------------------------------------------------------- +# 7zip Common (stream, coder infrastructure) +# --------------------------------------------------------------------------- +set(7Z_7ZIP_COMMON + ${SZ}/CPP/7zip/Common/CreateCoder.cpp + ${SZ}/CPP/7zip/Common/CWrappers.cpp + ${SZ}/CPP/7zip/Common/InBuffer.cpp + ${SZ}/CPP/7zip/Common/InOutTempBuffer.cpp + ${SZ}/CPP/7zip/Common/FilterCoder.cpp + ${SZ}/CPP/7zip/Common/LimitedStreams.cpp + ${SZ}/CPP/7zip/Common/LockedStream.cpp + ${SZ}/CPP/7zip/Common/MemBlocks.cpp + ${SZ}/CPP/7zip/Common/MethodId.cpp + ${SZ}/CPP/7zip/Common/MethodProps.cpp + ${SZ}/CPP/7zip/Common/OffsetStream.cpp + ${SZ}/CPP/7zip/Common/OutBuffer.cpp + ${SZ}/CPP/7zip/Common/OutMemStream.cpp + ${SZ}/CPP/7zip/Common/ProgressMt.cpp + ${SZ}/CPP/7zip/Common/ProgressUtils.cpp + ${SZ}/CPP/7zip/Common/PropId.cpp + ${SZ}/CPP/7zip/Common/StreamBinder.cpp + ${SZ}/CPP/7zip/Common/StreamObjects.cpp + ${SZ}/CPP/7zip/Common/StreamUtils.cpp + ${SZ}/CPP/7zip/Common/UniqBlocks.cpp + ${SZ}/CPP/7zip/Common/VirtThread.cpp +) + +# --------------------------------------------------------------------------- +# Archive format handlers (standalone) +# --------------------------------------------------------------------------- +set(7Z_ARCHIVE_HANDLERS + ${SZ}/CPP/7zip/Archive/ApfsHandler.cpp + ${SZ}/CPP/7zip/Archive/ApmHandler.cpp + ${SZ}/CPP/7zip/Archive/ArHandler.cpp + ${SZ}/CPP/7zip/Archive/ArjHandler.cpp + ${SZ}/CPP/7zip/Archive/Base64Handler.cpp + ${SZ}/CPP/7zip/Archive/Bz2Handler.cpp + ${SZ}/CPP/7zip/Archive/ComHandler.cpp + ${SZ}/CPP/7zip/Archive/CpioHandler.cpp + ${SZ}/CPP/7zip/Archive/CramfsHandler.cpp + ${SZ}/CPP/7zip/Archive/DeflateProps.cpp + ${SZ}/CPP/7zip/Archive/DmgHandler.cpp + ${SZ}/CPP/7zip/Archive/ElfHandler.cpp + ${SZ}/CPP/7zip/Archive/ExtHandler.cpp + ${SZ}/CPP/7zip/Archive/FatHandler.cpp + ${SZ}/CPP/7zip/Archive/FlvHandler.cpp + ${SZ}/CPP/7zip/Archive/GptHandler.cpp + ${SZ}/CPP/7zip/Archive/GzHandler.cpp + ${SZ}/CPP/7zip/Archive/HandlerCont.cpp + ${SZ}/CPP/7zip/Archive/HfsHandler.cpp + ${SZ}/CPP/7zip/Archive/IhexHandler.cpp + ${SZ}/CPP/7zip/Archive/LpHandler.cpp + ${SZ}/CPP/7zip/Archive/LzhHandler.cpp + ${SZ}/CPP/7zip/Archive/LzmaHandler.cpp + ${SZ}/CPP/7zip/Archive/MachoHandler.cpp + ${SZ}/CPP/7zip/Archive/MbrHandler.cpp + ${SZ}/CPP/7zip/Archive/MslzHandler.cpp + ${SZ}/CPP/7zip/Archive/MubHandler.cpp + ${SZ}/CPP/7zip/Archive/NtfsHandler.cpp + ${SZ}/CPP/7zip/Archive/PeHandler.cpp + ${SZ}/CPP/7zip/Archive/PpmdHandler.cpp + ${SZ}/CPP/7zip/Archive/QcowHandler.cpp + ${SZ}/CPP/7zip/Archive/RpmHandler.cpp + ${SZ}/CPP/7zip/Archive/SparseHandler.cpp + ${SZ}/CPP/7zip/Archive/SplitHandler.cpp + ${SZ}/CPP/7zip/Archive/SquashfsHandler.cpp + ${SZ}/CPP/7zip/Archive/SwfHandler.cpp + ${SZ}/CPP/7zip/Archive/UefiHandler.cpp + ${SZ}/CPP/7zip/Archive/VdiHandler.cpp + ${SZ}/CPP/7zip/Archive/VhdHandler.cpp + ${SZ}/CPP/7zip/Archive/VhdxHandler.cpp + ${SZ}/CPP/7zip/Archive/VmdkHandler.cpp + ${SZ}/CPP/7zip/Archive/XarHandler.cpp + ${SZ}/CPP/7zip/Archive/XzHandler.cpp + ${SZ}/CPP/7zip/Archive/ZHandler.cpp + ${SZ}/CPP/7zip/Archive/ZstdHandler.cpp +) + +# --------------------------------------------------------------------------- +# Archive Common +# --------------------------------------------------------------------------- +set(7Z_ARCHIVE_COMMON + ${SZ}/CPP/7zip/Archive/Common/CoderMixer2.cpp + ${SZ}/CPP/7zip/Archive/Common/DummyOutStream.cpp + ${SZ}/CPP/7zip/Archive/Common/FindSignature.cpp + ${SZ}/CPP/7zip/Archive/Common/InStreamWithCRC.cpp + ${SZ}/CPP/7zip/Archive/Common/ItemNameUtils.cpp + ${SZ}/CPP/7zip/Archive/Common/MultiStream.cpp + ${SZ}/CPP/7zip/Archive/Common/OutStreamWithCRC.cpp + ${SZ}/CPP/7zip/Archive/Common/OutStreamWithSha1.cpp + ${SZ}/CPP/7zip/Archive/Common/HandlerOut.cpp + ${SZ}/CPP/7zip/Archive/Common/ParseProperties.cpp +) + +# --------------------------------------------------------------------------- +# 7z format +# --------------------------------------------------------------------------- +set(7Z_FORMAT_7Z + ${SZ}/CPP/7zip/Archive/7z/7zCompressionMode.cpp + ${SZ}/CPP/7zip/Archive/7z/7zDecode.cpp + ${SZ}/CPP/7zip/Archive/7z/7zEncode.cpp + ${SZ}/CPP/7zip/Archive/7z/7zExtract.cpp + ${SZ}/CPP/7zip/Archive/7z/7zFolderInStream.cpp + ${SZ}/CPP/7zip/Archive/7z/7zHandler.cpp + ${SZ}/CPP/7zip/Archive/7z/7zHandlerOut.cpp + ${SZ}/CPP/7zip/Archive/7z/7zHeader.cpp + ${SZ}/CPP/7zip/Archive/7z/7zIn.cpp + ${SZ}/CPP/7zip/Archive/7z/7zOut.cpp + ${SZ}/CPP/7zip/Archive/7z/7zProperties.cpp + ${SZ}/CPP/7zip/Archive/7z/7zSpecStream.cpp + ${SZ}/CPP/7zip/Archive/7z/7zUpdate.cpp + ${SZ}/CPP/7zip/Archive/7z/7zRegister.cpp +) + +# --------------------------------------------------------------------------- +# CAB, CHM, ISO, NSIS, RAR, TAR, UDF, WIM, ZIP formats +# --------------------------------------------------------------------------- +set(7Z_FORMAT_CAB + ${SZ}/CPP/7zip/Archive/Cab/CabBlockInStream.cpp + ${SZ}/CPP/7zip/Archive/Cab/CabHandler.cpp + ${SZ}/CPP/7zip/Archive/Cab/CabHeader.cpp + ${SZ}/CPP/7zip/Archive/Cab/CabIn.cpp + ${SZ}/CPP/7zip/Archive/Cab/CabRegister.cpp +) +set(7Z_FORMAT_CHM + ${SZ}/CPP/7zip/Archive/Chm/ChmHandler.cpp + ${SZ}/CPP/7zip/Archive/Chm/ChmIn.cpp +) +set(7Z_FORMAT_ISO + ${SZ}/CPP/7zip/Archive/Iso/IsoHandler.cpp + ${SZ}/CPP/7zip/Archive/Iso/IsoHeader.cpp + ${SZ}/CPP/7zip/Archive/Iso/IsoIn.cpp + ${SZ}/CPP/7zip/Archive/Iso/IsoRegister.cpp +) +set(7Z_FORMAT_NSIS + ${SZ}/CPP/7zip/Archive/Nsis/NsisDecode.cpp + ${SZ}/CPP/7zip/Archive/Nsis/NsisHandler.cpp + ${SZ}/CPP/7zip/Archive/Nsis/NsisIn.cpp + ${SZ}/CPP/7zip/Archive/Nsis/NsisRegister.cpp +) +set(7Z_FORMAT_RAR + ${SZ}/CPP/7zip/Archive/Rar/RarHandler.cpp + ${SZ}/CPP/7zip/Archive/Rar/Rar5Handler.cpp +) +set(7Z_FORMAT_TAR + ${SZ}/CPP/7zip/Archive/Tar/TarHandler.cpp + ${SZ}/CPP/7zip/Archive/Tar/TarHandlerOut.cpp + ${SZ}/CPP/7zip/Archive/Tar/TarHeader.cpp + ${SZ}/CPP/7zip/Archive/Tar/TarIn.cpp + ${SZ}/CPP/7zip/Archive/Tar/TarOut.cpp + ${SZ}/CPP/7zip/Archive/Tar/TarUpdate.cpp + ${SZ}/CPP/7zip/Archive/Tar/TarRegister.cpp +) +set(7Z_FORMAT_UDF + ${SZ}/CPP/7zip/Archive/Udf/UdfHandler.cpp + ${SZ}/CPP/7zip/Archive/Udf/UdfIn.cpp +) +set(7Z_FORMAT_WIM + ${SZ}/CPP/7zip/Archive/Wim/WimHandler.cpp + ${SZ}/CPP/7zip/Archive/Wim/WimHandlerOut.cpp + ${SZ}/CPP/7zip/Archive/Wim/WimIn.cpp + ${SZ}/CPP/7zip/Archive/Wim/WimRegister.cpp +) +set(7Z_FORMAT_ZIP + ${SZ}/CPP/7zip/Archive/Zip/ZipAddCommon.cpp + ${SZ}/CPP/7zip/Archive/Zip/ZipHandler.cpp + ${SZ}/CPP/7zip/Archive/Zip/ZipHandlerOut.cpp + ${SZ}/CPP/7zip/Archive/Zip/ZipIn.cpp + ${SZ}/CPP/7zip/Archive/Zip/ZipItem.cpp + ${SZ}/CPP/7zip/Archive/Zip/ZipOut.cpp + ${SZ}/CPP/7zip/Archive/Zip/ZipUpdate.cpp + ${SZ}/CPP/7zip/Archive/Zip/ZipRegister.cpp +) + +# --------------------------------------------------------------------------- +# Compression codecs +# --------------------------------------------------------------------------- +set(7Z_COMPRESS + ${SZ}/CPP/7zip/Compress/Bcj2Coder.cpp + ${SZ}/CPP/7zip/Compress/Bcj2Register.cpp + ${SZ}/CPP/7zip/Compress/BcjCoder.cpp + ${SZ}/CPP/7zip/Compress/BcjRegister.cpp + ${SZ}/CPP/7zip/Compress/BitlDecoder.cpp + ${SZ}/CPP/7zip/Compress/BranchMisc.cpp + ${SZ}/CPP/7zip/Compress/BranchRegister.cpp + ${SZ}/CPP/7zip/Compress/ByteSwap.cpp + ${SZ}/CPP/7zip/Compress/BZip2Crc.cpp + ${SZ}/CPP/7zip/Compress/BZip2Decoder.cpp + ${SZ}/CPP/7zip/Compress/BZip2Encoder.cpp + ${SZ}/CPP/7zip/Compress/BZip2Register.cpp + ${SZ}/CPP/7zip/Compress/CopyCoder.cpp + ${SZ}/CPP/7zip/Compress/CopyRegister.cpp + ${SZ}/CPP/7zip/Compress/Deflate64Register.cpp + ${SZ}/CPP/7zip/Compress/DeflateDecoder.cpp + ${SZ}/CPP/7zip/Compress/DeflateEncoder.cpp + ${SZ}/CPP/7zip/Compress/DeflateRegister.cpp + ${SZ}/CPP/7zip/Compress/DeltaFilter.cpp + ${SZ}/CPP/7zip/Compress/ImplodeDecoder.cpp + ${SZ}/CPP/7zip/Compress/LzfseDecoder.cpp + ${SZ}/CPP/7zip/Compress/LzhDecoder.cpp + ${SZ}/CPP/7zip/Compress/Lzma2Decoder.cpp + ${SZ}/CPP/7zip/Compress/Lzma2Encoder.cpp + ${SZ}/CPP/7zip/Compress/Lzma2Register.cpp + ${SZ}/CPP/7zip/Compress/LzmaDecoder.cpp + ${SZ}/CPP/7zip/Compress/LzmaEncoder.cpp + ${SZ}/CPP/7zip/Compress/LzmaRegister.cpp + ${SZ}/CPP/7zip/Compress/LzmsDecoder.cpp + ${SZ}/CPP/7zip/Compress/LzOutWindow.cpp + ${SZ}/CPP/7zip/Compress/LzxDecoder.cpp + ${SZ}/CPP/7zip/Compress/PpmdDecoder.cpp + ${SZ}/CPP/7zip/Compress/PpmdEncoder.cpp + ${SZ}/CPP/7zip/Compress/PpmdRegister.cpp + ${SZ}/CPP/7zip/Compress/PpmdZip.cpp + ${SZ}/CPP/7zip/Compress/QuantumDecoder.cpp + ${SZ}/CPP/7zip/Compress/ShrinkDecoder.cpp + ${SZ}/CPP/7zip/Compress/XpressDecoder.cpp + ${SZ}/CPP/7zip/Compress/XzDecoder.cpp + ${SZ}/CPP/7zip/Compress/XzEncoder.cpp + ${SZ}/CPP/7zip/Compress/ZlibDecoder.cpp + ${SZ}/CPP/7zip/Compress/ZlibEncoder.cpp + ${SZ}/CPP/7zip/Compress/ZDecoder.cpp + ${SZ}/CPP/7zip/Compress/ZstdDecoder.cpp + # RAR codecs + ${SZ}/CPP/7zip/Compress/Rar1Decoder.cpp + ${SZ}/CPP/7zip/Compress/Rar2Decoder.cpp + ${SZ}/CPP/7zip/Compress/Rar3Decoder.cpp + ${SZ}/CPP/7zip/Compress/Rar3Vm.cpp + ${SZ}/CPP/7zip/Compress/Rar5Decoder.cpp + ${SZ}/CPP/7zip/Compress/RarCodecsRegister.cpp +) + +# --------------------------------------------------------------------------- +# Crypto +# --------------------------------------------------------------------------- +set(7Z_CRYPTO + ${SZ}/CPP/7zip/Crypto/7zAes.cpp + ${SZ}/CPP/7zip/Crypto/7zAesRegister.cpp + ${SZ}/CPP/7zip/Crypto/HmacSha1.cpp + ${SZ}/CPP/7zip/Crypto/HmacSha256.cpp + ${SZ}/CPP/7zip/Crypto/MyAes.cpp + ${SZ}/CPP/7zip/Crypto/MyAesReg.cpp + ${SZ}/CPP/7zip/Crypto/Pbkdf2HmacSha1.cpp + ${SZ}/CPP/7zip/Crypto/RandGen.cpp + ${SZ}/CPP/7zip/Crypto/WzAes.cpp + ${SZ}/CPP/7zip/Crypto/ZipCrypto.cpp + ${SZ}/CPP/7zip/Crypto/ZipStrong.cpp + # RAR crypto + ${SZ}/CPP/7zip/Crypto/Rar20Crypto.cpp + ${SZ}/CPP/7zip/Crypto/Rar5Aes.cpp + ${SZ}/CPP/7zip/Crypto/RarAes.cpp +) + +# --------------------------------------------------------------------------- +# DLL export entry points (what the archive library dlopen's) +# --------------------------------------------------------------------------- +set(7Z_EXPORTS + ${SZ}/CPP/7zip/Archive/ArchiveExports.cpp + ${SZ}/CPP/7zip/Archive/DllExports2.cpp + ${SZ}/CPP/7zip/Compress/CodecExports.cpp +) + +# --------------------------------------------------------------------------- +# Build the shared library +# --------------------------------------------------------------------------- +add_library(7z SHARED + ${7Z_C_SOURCES} + ${7Z_CPP_COMMON} + ${7Z_CPP_WINDOWS} + ${7Z_7ZIP_COMMON} + ${7Z_ARCHIVE_HANDLERS} + ${7Z_ARCHIVE_COMMON} + ${7Z_FORMAT_7Z} + ${7Z_FORMAT_CAB} + ${7Z_FORMAT_CHM} + ${7Z_FORMAT_ISO} + ${7Z_FORMAT_NSIS} + ${7Z_FORMAT_RAR} + ${7Z_FORMAT_TAR} + ${7Z_FORMAT_UDF} + ${7Z_FORMAT_WIM} + ${7Z_FORMAT_ZIP} + ${7Z_COMPRESS} + ${7Z_CRYPTO} + ${7Z_EXPORTS} +) + +# Output as "7z.so" (not "lib7z.so") +set_target_properties(7z PROPERTIES + PREFIX "" + OUTPUT_NAME "7z" + POSITION_INDEPENDENT_CODE ON +) + +target_include_directories(7z PRIVATE + ${SZ}/CPP + ${SZ}/CPP/Common + ${SZ}/CPP/Windows + ${SZ}/CPP/7zip + ${SZ}/C +) + +target_compile_definitions(7z PRIVATE + -DZ7_EXTERNAL_CODECS + -DNDEBUG + -D_REENTRANT + -D_FILE_OFFSET_BITS=64 + -D_LARGEFILE_SOURCE +) + +target_compile_options(7z PRIVATE + -O2 + -Wno-error + -Wno-unused-parameter + -Wno-sign-compare + -Wno-missing-field-initializers + -Wno-reorder +) + +target_link_libraries(7z PRIVATE pthread dl) + +# Install 7z.so next to the organizer binary (in dlls/ subdirectory) +install(TARGETS 7z LIBRARY DESTINATION dlls) +# Also install to the bin root so dlopen can find it easily +install(TARGETS 7z LIBRARY DESTINATION bin) diff --git a/libs/archive/.clang-format b/libs/archive/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/archive/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/archive/.git-blame-ignore-revs b/libs/archive/.git-blame-ignore-revs new file mode 100644 index 0000000..be37b84 --- /dev/null +++ b/libs/archive/.git-blame-ignore-revs @@ -0,0 +1 @@ +bc77d2a270cc24fd69a8b46ccad4cca2b334a19d diff --git a/libs/archive/.gitattributes b/libs/archive/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/archive/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/archive/.github/workflows/build.yml b/libs/archive/.github/workflows/build.yml new file mode 100644 index 0000000..f476ad1 --- /dev/null +++ b/libs/archive/.github/workflows/build.yml @@ -0,0 +1,39 @@ +name: Build Archive + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + runs-on: windows-2022 + steps: + # set VCPKG Root + - name: "Set environmental variables" + shell: bash + run: | + echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> $GITHUB_ENV + + - uses: actions/checkout@v4 + + - name: Configure Archive build + shell: pwsh + run: | + cmake --preset vs2022-windows-shared "-DCMAKE_INSTALL_PREFIX=install" + + - name: Build Archive + run: cmake --build vsbuild --config RelWithDebInfo + + - name: Install Archive + run: cmake --install vsbuild --config RelWithDebInfo + + - name: Upload Archive artifact + uses: actions/upload-artifact@master + with: + name: archive + path: ./install diff --git a/libs/archive/.github/workflows/linting.yml b/libs/archive/.github/workflows/linting.yml new file mode 100644 index 0000000..22eb21a --- /dev/null +++ b/libs/archive/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Archive + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/archive/.gitignore b/libs/archive/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/archive/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/archive/.pre-commit-config.yaml b/libs/archive/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/archive/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/archive/CMakeLists.txt b/libs/archive/CMakeLists.txt new file mode 100644 index 0000000..fa5d783 --- /dev/null +++ b/libs/archive/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.16) + +include(CMakePackageConfigHelpers) + +project(archive) + +set_property(GLOBAL PROPERTY USE_FOLDERS ON) +option(BUILD_SHARED_LIBS "Build using shared libraries" ON) + +add_subdirectory(src) + +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/mo2-archive-config.cmake" + INSTALL_DESTINATION "lib/cmake/mo2-archive" + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO +) + +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/src/version.rc" archive_version) +string(REGEX MATCH "#define VER_FILEVERSION[ \t]*([0-9]+),([0-9]+),([0-9]+)" _ ${archive_version}) +set(archive_version_major ${CMAKE_MATCH_1}) +set(archive_version_minor ${CMAKE_MATCH_2}) +set(archive_version_patch ${CMAKE_MATCH_3}) + +message(STATUS "[MO2] Found version '${archive_version_major}.${archive_version_minor}.${archive_version_patch}'.") + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/mo2-archive-config-version.cmake" + VERSION "${archive_version_major}.${archive_version_minor}.${archive_version_patch}" + COMPATIBILITY AnyNewerVersion +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/mo2-archive-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/mo2-archive-config-version.cmake + DESTINATION lib/cmake/mo2-archive +) diff --git a/libs/archive/CMakePresets.json b/libs/archive/CMakePresets.json new file mode 100644 index 0000000..ed1f0cc --- /dev/null +++ b/libs/archive/CMakePresets.json @@ -0,0 +1,74 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + }, + "BUILD_SHARED_LIBS": { + "type": "BOOL", + "value": "OFF" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg"], + "name": "vs2022-windows-static", + "toolset": "v143" + }, + { + "cacheVariables": { + "BUILD_SHARED_LIBS": { + "type": "BOOL", + "value": "ON" + }, + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows" + } + }, + "inherits": "vs2022-windows-static", + "name": "vs2022-windows-shared" + }, + { + "inherits": "vs2022-windows-shared", + "name": "vs2022-windows" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/archive/LICENSE b/libs/archive/LICENSE new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/libs/archive/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/libs/archive/README.md b/libs/archive/README.md new file mode 100644 index 0000000..e389081 --- /dev/null +++ b/libs/archive/README.md @@ -0,0 +1,239 @@ +[![Build status](https://ci.appveyor.com/api/projects/status/hdthueiiuedeb38f?svg=true)](https://ci.appveyor.com/project/Modorganizer2/modorganizer-archive) + +# modorganizer-archive + +This module provides a wrapper round the 7zip `7z.dll` allowing easy(ish) access to the contents of an archive. + +## How to build? + +If you want to build this as par of ModOrganizer2, simply use ModOrganizer2 build system. + +If you want to build this as a standalone DLL, you can run the following (requires `cmake >= 3.16`): + +```batch +mkdir build +cd build +cmake .. +cmake --build . --config Release +``` + +This will download the two required dependencies (7z sources and [`fmtlib`](https://github.com/fmtlib/fmt)), and +build the DLL under `build/src/Release`. + +In order to use the DLL, you need to have the `7z.dll` available in your path, otherwize `CreateArchive()` will +always fail. You can get the `7z.dll` by installing `7z.exe` or by building 7z yourself (building `archive` does +not build `7z`). + +## The `Archive` class + +The first thing to do is to create an archive wrapper: + +```cpp +#include + +std::unique_ptr CreateArchive(); +``` + +This creates an archive handler. + +You can check the [src/archive.h](src/archive.h) header for more details but here are some +of the available methods: + +```cpp +/** + * @brief Check if this Archive wrapper is in a valid state. + * + * A non-valid Archive instance usually means that the 7z DLLs could not be loaded properly. Failures + * to open or extract archives do not invalidate the Archive, so this should only be used to check + * if the Archive object has been initialized properly. + * + * @return true if this instance is valid, false otherwise. + */ +bool Archive::isValid() const; +``` + +If this returns `false`, this probably means the system cannot find `7z.dll` or it is corrupt, very old (or possibly too new). +**You should check this before calling `open`.** + +```cpp +/** + * @brief Open the given archive. + * + * @param archivePath Path to the archive to open. + * @param passwordCallback Callback to use to ask user for password. This callback must remain + * valid until extraction is complete since some types of archives only requires password when + * extracting. + * + * @return true if the archive was open properly, false otherwise. + */ +bool Archive::open(std::wstring const &archiveName, ArchiveCallbacks::PasswordCallback passwordCallback) +``` + +This attempts to open the specified archive file. It should manage to open pretty much anything 7zip recognises. It returns `true` on success. +If an error occurs, it returns `false` and `getLastError()` can be used to check the cause of the failure (see the `Archive.h` header for the +list of possible errors): + +```cpp +Archive::Error getLastError() const; +``` + +If `std::wstring passwordChangeCallback()` is not empty, it is called if when password is needed and should return the password to use. + +**Note:** this may be called during `extract` rather than during `open`, so should remain usable until the end of the extraction. +If you do not supply this callback, archives with passwords will be unreadable. + +Once the archive is opned, you can retrieve the list of files inside using `getFileList()`: + +```cpp +const std::vector& getFileList() const; +``` + +This will return a reference to a vector containing `FileData`. The vector contains non-const pointers so you can actually modify (not assign) +the pointed `FileData` to indicate which files to extract and the path to extract them to. + +Once you have updated the `FileData` (see below) you want to extract, you can then perform the extraction using: + +```cpp +/** + * @brief Extract the content of the archive. + * + * This function uses the filenames from FileData to obtain the extraction paths of file. + * + * @param outputDirectory Path to the directory where the archive should be extracted. If not empty, + * conflicting files will be replaced by the extracted ones. + * @param progressCallback Function called to notify extraction progress. This function is called + * when new data is written to the disk, not when the archive is read, so it may take a little + * time to start. + * @param fileChangeCallback Function called when the file currently being extracted changes. + * @param errorCallback Function called when an error occurs. + * + * @return true if the archive was extracted, false otherwise. + */ +bool extract(std::wstring const &outputDirectory, ArchiveCallbacks::ProgressCallback progressCallback, + ArchiveCallbacks::FileChangeCallback fileChangeCallback, ArchiveCallbacks::ErrorCallback errorCallback) +``` + +All callbacks are optional, you can pass an empty `std::function` instead (either `nullptr` or `{}`). The purpose of the callbacks: + +- `progressCallback(float)` is called during extraction to notify progress. +- `fileChangeCallback(std::wstring const&)` is called when a file starts being extracted. +- `errorCallback(std::wstring const&)` is called if an error occurred, with an appropriate error message. There is not much you can do + here beyond displaying the message. This will also result in a failure return from `extract`. + +Once `extract()` is done, you can call `getFileList()` again and perform a different extractions. `extract()` will clean the list of +`FileData` (unless an error occurred). + +You can cancel the extraction at any time by calling: + +```cpp +void Archive::cancel(); +``` + +This will cause `extract` to return `false` and `getLastError` to return `ERROR_EXTRACT_CANCELLED`. + +Once you are done, do not forget to close the currently opened `Archive`: + +```cpp +void Archive::close(); +``` + +## The `FileData` class + +As you have seen above, the `getFileList` method returns a reference to a vector of entries about all the files in the archive. +You can see the full declaration of `FileData` in [src/archive.h](src/archive.h). The following methods are the most important +ones: + +```cpp +void FileData::addOutputFileName(std::wstring const& filepath) +``` + +Adds a new output path for this file. The given `filepath` should be relative to the extraction folder specified in `Archive::extract`. +Initially, the list of output paths is empty, so if you do not call `addOutputFileName`, the corresponding file will not be extracted. +You can extract a file in the archive to as many files as you want. + +```cpp +std::vector FileData::getAndClearOutputFileNames() +``` + +Returns the list of output paths (relative to the extraction folder) for this file and clears it. This is normally only used inside +`extract` but can be used to clear the output filenames after a failure. + +Depending on the type of archives, you may have entries corresponding to directories, in which case `FileData::isDirectory()` will +return `true`. +You can "extract" those like normal files, but directories will be automatically created for files if necessary anyway. + +## Full example + +Below is a full example on how to extract an archive to a given folder: + +```cpp +#include + +#include "archive.h" + +int main() { + + // Path to the archive and to the output folder: + const std::wstring archivePath = L"archive.7z"; + const std::wstring outputFolder = L"output"; + + auto archive = CreateArchive(); + + if (!archive->isValid()) { + std::wcerr << "Failed to load the archive module: " << archive->getLastError() << '\n'; + return -1; + } + + // You can set a log callback if you want: + archive->setLogCallback([](auto level, auto const& message) { + std::wcout << message << '\n'; + }); + + // Open the archive: + if (!archive->open(archivePath, nullptr)) { + std::wcerr << "Failed to open the archive: " << archive->getLastError() << '\n'; + return -1; + } + + // Get the list of files: + auto const& files = archive->getFileList(); + + // Mark all files for extraction to their path in the archive: + for (auto *fileData: files) { + fileData->addOutputFileName(fileData->getFileName()); + } + + // Extract everything (without callbacks): + auto result = archive->extract(outputFolder, nullptr, nullptr, nullptr); + + if (!result) { + std::wcerr << "Failed to extract the archive: " << archive->getLastError() << '\n'; + return -1; + } + + // Close the archive: + archive->close(); + + return 0; +} +``` + +# Copyright + +Copyright (C) 2012 Sebastian Herbord, (C) 2020 MO2 Team. 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 + +See [LICENSE](LICENSE) for more details. diff --git a/libs/archive/cmake/config.cmake.in b/libs/archive/cmake/config.cmake.in new file mode 100644 index 0000000..4bf70e6 --- /dev/null +++ b/libs/archive/cmake/config.cmake.in @@ -0,0 +1,7 @@ +@PACKAGE_INIT@ + +if(WIN32) + find_package(7zip CONFIG REQUIRED) +endif() + +include ( "${CMAKE_CURRENT_LIST_DIR}/mo2-archive-targets.cmake" ) diff --git a/libs/archive/include/archive/archive.h b/libs/archive/include/archive/archive.h new file mode 100644 index 0000000..3985427 --- /dev/null +++ b/libs/archive/include/archive/archive.h @@ -0,0 +1,280 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 ARCHIVE_H +#define ARCHIVE_H + +#include +#include +#include +#include + +#if defined(MO2_ARCHIVE_BUILD_STATIC) +#define DLLEXPORT +#elif defined(_WIN32) + #if defined(MO2_ARCHIVE_BUILD_EXPORT) + #define DLLEXPORT __declspec(dllexport) + #else + #define DLLEXPORT __declspec(dllimport) + #endif +#else + #if defined(MO2_ARCHIVE_BUILD_EXPORT) + #define DLLEXPORT __attribute__((visibility("default"))) + #else + #define DLLEXPORT + #endif +#endif + +class FileData +{ +public: + /** + * @return the path of this entry in the archive (usually relative, unless the archive + * contains absolute path). + */ + virtual std::wstring getArchiveFilePath() const = 0; + + /** + * @return the size of this entry in bytes (uncompressed). + */ + virtual uint64_t getSize() const = 0; + + /** + * @brief Add the given filepath to the list of files to create from this + * entry when extracting. + * + * @param filepath The filepath to add, relative to the output folder. + */ + virtual void addOutputFilePath(std::wstring const& filepath) = 0; + + /** + * @brief Retrieve the list of filepaths to extract this entry to. + * + * @return the list of paths this entry should be extracted to, relative to the + * output folder. + */ + virtual const std::vector& getOutputFilePaths() const = 0; + + /** + * @brief Clear the list of output file paths for this entry. + */ + virtual void clearOutputFilePaths() = 0; + + /** + * @return the CRC of this file. + */ + virtual uint64_t getCRC() const = 0; + + /** + * @return true if this entry is a directory, false otherwize. + */ + virtual bool isDirectory() const = 0; + + virtual ~FileData() = default; +}; + +class Archive +{ +public: // Declarations + enum class LogLevel + { + Debug, + Info, + Warning, + Error + }; + + enum class ProgressType + { + + // Indicates the 7z progression in the archive (related to reading the archive. When + // extracting + // a lot of files, this may reach 100% way before the extraction is complete since + // most of the + // time will be spend writing data and not reading it (use EXTRACTION in this case). + // When + // extracting few small files, this may be useful for solid archives since most of + // the time + // will be spent in reading and decompressing the archive rather than in writing the + // actual files. + ARCHIVE, + + // Progress about extraction. If this reach 100%, it means that the extraction of + // all files is + // complete. The EXTRACTION progress may not start immediately, and might be kind of + // chaotic when + // extracting few files from an archive, but is much more representative of the + // actual progress + // than ARCHIVE. + EXTRACTION + }; + + enum class FileChangeType + { + EXTRACTION_START, + EXTRACTION_END + }; + + static constexpr int MAX_PASSWORD_LENGTH = 256; + + /** + * List of callbacks: + */ + using LogCallback = std::function; + using ProgressCallback = std::function; + using PasswordCallback = std::function; + using FileChangeCallback = std::function; + using ErrorCallback = std::function; + + /** + * + */ + enum class Error + { + ERROR_NONE, + ERROR_EXTRACT_CANCELLED, + ERROR_LIBRARY_NOT_FOUND, + ERROR_LIBRARY_INVALID, + ERROR_ARCHIVE_NOT_FOUND, + ERROR_FAILED_TO_OPEN_ARCHIVE, + ERROR_INVALID_ARCHIVE_FORMAT, + ERROR_LIBRARY_ERROR, + ERROR_ARCHIVE_INVALID, + ERROR_OUT_OF_MEMORY + }; + +public: // Special member functions: + virtual ~Archive() {} + +public: + /** + * @brief Check if this Archive wrapper is in a valid state. + * + * A non-valid Archive instance usually means that the 7z DLLs could not be loaded + * properly. Failures to open or extract archives do not invalidate the Archive, so + * this should only be used to check if the Archive object has been initialized + * properly. + * + * @return true if this instance is valid, false otherwise. + */ + virtual bool isValid() const = 0; + + /** + * @return retrieve the error-code of the last error that occurred. + */ + virtual Error getLastError() const = 0; + + /** + * @brief Set the callback used to log messages. + * + * To remove the callback, you can pass a default-constructed LogCallback object. + * + * @param logCallback The new callback to use for logging message. + */ + virtual void setLogCallback(LogCallback logCallback) = 0; + + /** + * @brief Open the given archive. + * + * @param archivePath Path to the archive to open. + * @param passwordCallback Callback to use to ask user for password. This callback + * must remain valid until extraction is complete since some types of archives only + * requires password when extracting. + * + * @return true if the archive was open properly, false otherwise. + */ + virtual bool open(std::wstring const& archivePath, + PasswordCallback passwordCallback) = 0; + + /** + * @brief Close the currently opened archive. + */ + virtual void close() = 0; + + /** + * @return the list of files in the currently opened archive. + */ + virtual const std::vector& getFileList() const = 0; + + /** + * @brief Extract the content of the archive. + * + * This function uses the filenames from FileData to obtain the extraction paths of + * file. All the callbacks are optional (you can specify default-constructed + * std::function). Overloads with one or two callbacks are also provided. + * + * @param outputDirectory Path to the directory where the archive should be extracted. + * If not empty, conflicting files will be replaced by the extracted ones. + * @param progressCallback Function called to notify extraction progress. + * @param fileChangeCallback Function called when the file currently being extracted + * changes. + * @param errorCallback Function called when an error occurs. + * + * @return true if the archive was extracted, false otherwise. + */ + virtual bool extract(std::wstring const& outputDirectory, + ProgressCallback progressCallback, + FileChangeCallback fileChangeCallback, + ErrorCallback errorCallback) = 0; + + /** + * @brief Cancel the current extraction process. + */ + virtual void cancel() = 0; + + // A bunch of useful overloads (with one or two callbacks): + bool extract(std::wstring const& outputDirectory, ErrorCallback errorCallback) + { + return extract(outputDirectory, {}, {}, errorCallback); + } + bool extract(std::wstring const& outputDirectory, ProgressCallback progressCallback) + { + return extract(outputDirectory, progressCallback, {}, {}); + } + bool extract(std::wstring const& outputDirectory, + FileChangeCallback fileChangeCallback) + { + return extract(outputDirectory, {}, fileChangeCallback, {}); + } + bool extract(std::wstring const& outputDirectory, ProgressCallback progressCallback, + ErrorCallback errorCallback) + { + return extract(outputDirectory, progressCallback, {}, errorCallback); + } + bool extract(std::wstring const& outputDirectory, ProgressCallback progressCallback, + FileChangeCallback fileChangeCallback) + { + return extract(outputDirectory, progressCallback, fileChangeCallback, {}); + } + bool extract(std::wstring const& outputDirectory, + FileChangeCallback fileChangeCallback, ErrorCallback errorCallback) + { + return extract(outputDirectory, {}, fileChangeCallback, errorCallback); + } +}; + +/** + * @brief Factory function for archive-objects. + * + * @return a pointer to a new Archive object that can be used to manipulate archives. + */ +DLLEXPORT std::unique_ptr CreateArchive(); + +#endif // ARCHIVE_H diff --git a/libs/archive/src/CMakeLists.txt b/libs/archive/src/CMakeLists.txt new file mode 100644 index 0000000..75e9b53 --- /dev/null +++ b/libs/archive/src/CMakeLists.txt @@ -0,0 +1,101 @@ +cmake_minimum_required(VERSION 3.16) + +if(WIN32) + find_package(7zip CONFIG REQUIRED) +endif() + +add_library(archive) + +set_target_properties(archive PROPERTIES CXX_STANDARD 20) + +# On Linux, we use the bundled 7zip SDK headers and dlopen the 7z.so at runtime +# On Windows, we use the vcpkg 7zip package +if(WIN32) + target_link_libraries(archive PRIVATE 7zip::7zip) +else() + # Add 7zip SDK headers from thirdparty + target_include_directories(archive PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/../thirdparty/CPP + ) + # We need to compile MyWindows.cpp for BSTR/Variant support on Linux + target_sources(archive PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/../thirdparty/CPP/Common/MyWindows.cpp + ) + target_link_libraries(archive PRIVATE dl) + target_compile_definitions(archive PRIVATE -DARCHIVE_LINUX_PORT) +endif() + +set(ARCHIVE_SOURCES + archive.cpp + compat.h + extractcallback.cpp + extractcallback.h + fileio.cpp + fileio.h + formatter.h + inputstream.cpp + inputstream.h + instrument.h + interfaceguids.cpp + library.h + multioutputstream.cpp + multioutputstream.h + opencallback.cpp + opencallback.h + propertyvariant.cpp + propertyvariant.h + unknown_impl.h +) + +if(WIN32) + list(APPEND ARCHIVE_SOURCES version.rc) +endif() + +target_sources(archive + PRIVATE + ${ARCHIVE_SOURCES} + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_LIST_DIR}/../include + FILES ${CMAKE_CURRENT_LIST_DIR}/../include/archive/archive.h +) + +target_include_directories(archive PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../include/archive) + +if (MSVC) + target_compile_options(archive + PRIVATE + "/MP" + "/W4" + "/external:anglebrackets" + "/external:W0" + ) + target_link_options(archive + PRIVATE + $<$:/LTCG /INCREMENTAL:NO /OPT:REF /OPT:ICF> + ) + + set_target_properties(archive PROPERTIES VS_STARTUP_PROJECT archive) +endif() + +if(NOT MSVC) + target_compile_options(archive PRIVATE -Wall -Wextra -Wno-unused-parameter) +endif() + +if (BUILD_STATIC) + target_compile_definitions(archive PUBLIC -DMO2_ARCHIVE_BUILD_STATIC) +else() + target_compile_definitions(archive PRIVATE -DMO2_ARCHIVE_BUILD_EXPORT) +endif() + +add_library(mo2::archive ALIAS archive) + +install(TARGETS archive EXPORT archiveTargets FILE_SET HEADERS) +if (NOT BUILD_STATIC AND WIN32) + install(FILES $ DESTINATION pdb OPTIONAL) +endif() +install(EXPORT archiveTargets + FILE mo2-archive-targets.cmake + NAMESPACE mo2:: + DESTINATION lib/cmake/mo2-archive +) diff --git a/libs/archive/src/archive.cpp b/libs/archive/src/archive.cpp new file mode 100644 index 0000000..6d35366 --- /dev/null +++ b/libs/archive/src/archive.cpp @@ -0,0 +1,609 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 "archive.h" +#include "compat.h" + +#include "extractcallback.h" +#include "inputstream.h" +#include "library.h" +#include "opencallback.h" +#include "propertyvariant.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace PropID = NArchive::NHandlerPropID; + +class FileDataImpl : public FileData +{ + friend class Archive; + +public: + FileDataImpl(std::wstring const& fileName, UInt64 size, UInt64 crc, bool isDirectory) + : m_FileName(fileName), m_Size(size), m_CRC(crc), m_IsDirectory(isDirectory) + {} + + virtual std::wstring getArchiveFilePath() const override { return m_FileName; } + virtual uint64_t getSize() const override { return m_Size; } + + virtual void addOutputFilePath(std::wstring const& fileName) override + { + m_OutputFilePaths.push_back(fileName); + } + virtual const std::vector& getOutputFilePaths() const override + { + return m_OutputFilePaths; + } + + virtual void clearOutputFilePaths() override { m_OutputFilePaths.clear(); } + + bool isEmpty() const { return m_OutputFilePaths.empty(); } + virtual bool isDirectory() const override { return m_IsDirectory; } + virtual uint64_t getCRC() const override { return m_CRC; } + +private: + std::wstring m_FileName; + UInt64 m_Size; + UInt64 m_CRC; + std::vector m_OutputFilePaths; + bool m_IsDirectory; +}; + +/// represents the connection to one archive and provides common functionality +class ArchiveImpl : public Archive +{ + + // Callback that does nothing but avoid having to check if the callback is present + // everytime. + static LogCallback DefaultLogCallback; + +public: + ArchiveImpl(); + virtual ~ArchiveImpl(); + + virtual bool isValid() const { return m_Valid; } + + virtual Error getLastError() const { return m_LastError; } + virtual void setLogCallback(LogCallback logCallback) override + { + // Wrap the callback so that we do not have to check if it is set everywhere: + m_LogCallback = logCallback ? logCallback : DefaultLogCallback; + } + + virtual bool open(std::wstring const& archiveName, + PasswordCallback passwordCallback) override; + virtual void close() override; + const std::vector& getFileList() const override { return m_FileList; } + virtual bool extract(std::wstring const& outputDirectory, + ProgressCallback progressCallback, + FileChangeCallback fileChangeCallback, + ErrorCallback errorCallback) override; + + virtual void cancel() override; + +private: + void clearFileList(); + void resetFileList(); + + HRESULT loadFormats(); + +private: + typedef UINT32(WINAPI* CreateObjectFunc)(const GUID* clsID, const GUID* interfaceID, + void** outObject); + CreateObjectFunc m_CreateObjectFunc; + + // A note: In 7zip source code this not is what this typedef is called, the old + // GetHandlerPropertyFunc appears to be deprecated. + typedef UInt32(WINAPI* GetPropertyFunc)(UInt32 index, PROPID propID, + PROPVARIANT* value); + GetPropertyFunc m_GetHandlerPropertyFunc; + + template + T readHandlerProperty(UInt32 index, PROPID propID) const; + + template + T readProperty(UInt32 index, PROPID propID) const; + + bool m_Valid; + Error m_LastError; + + ALibrary m_Library; + std::wstring m_ArchiveName; // TBH I don't think this is required + CComPtr m_ArchivePtr; + CArchiveExtractCallback* m_ExtractCallback; + + LogCallback m_LogCallback; + PasswordCallback m_PasswordCallback; + + std::vector m_FileList; + + std::wstring m_Password; + + struct ArchiveFormatInfo + { + CLSID m_ClassID; + std::wstring m_Name; + std::vector m_Signatures; + std::wstring m_Extensions; + std::wstring m_AdditionalExtensions; + UInt32 m_SignatureOffset; + }; + + typedef std::vector Formats; + Formats m_Formats; + + typedef std::unordered_map FormatMap; + FormatMap m_FormatMap; + + // I don't think one signature could possibly describe two formats. + typedef std::map SignatureMap; + SignatureMap m_SignatureMap; + + std::size_t m_MaxSignatureLen = 0; +}; + +Archive::LogCallback ArchiveImpl::DefaultLogCallback([](LogLevel, std::wstring const&) { +}); + +template +T ArchiveImpl::readHandlerProperty(UInt32 index, PROPID propID) const +{ + PropertyVariant prop; + if (m_GetHandlerPropertyFunc(index, propID, &prop) != S_OK) { + throw std::runtime_error("Failed to read property"); + } + return static_cast(prop); +} + +template +T ArchiveImpl::readProperty(UInt32 index, PROPID propID) const +{ + PropertyVariant prop; + if (m_ArchivePtr->GetProperty(index, propID, &prop) != S_OK) { + throw std::runtime_error("Failed to read property"); + } + return static_cast(prop); +} + +// Seriously, there is one format returned in the list that has no registered +// extension and no signature. WTF? +HRESULT ArchiveImpl::loadFormats() +{ + typedef UInt32(WINAPI * GetNumberOfFormatsFunc)(UInt32 * numFormats); + GetNumberOfFormatsFunc getNumberOfFormats = + m_Library.resolve("GetNumberOfFormats"); + if (getNumberOfFormats == nullptr) { + return E_FAIL; + } + + UInt32 numFormats; + RINOK(getNumberOfFormats(&numFormats)); + + for (UInt32 i = 0; i < numFormats; ++i) { + ArchiveFormatInfo item; + + item.m_Name = readHandlerProperty(i, PropID::kName); + + item.m_ClassID = readHandlerProperty(i, PropID::kClassID); + + // Should split up the extensions and map extension to type, and see what we get + // from that for preference then try all extensions anyway... + item.m_Extensions = readHandlerProperty(i, PropID::kExtension); + + // This is unnecessary currently for our purposes. Basically, for each + // extension, there's an 'addext' which, if set (to other than *) means that + // theres a double encoding going on. For instance, the bzip format is like this + // addext = "* * .tar .tar" + // ext = "bz2 bzip2 tbz2 tbz" + // which means that tbz2 and tbz should uncompress to a tar file which can be + // further processed as if it were a tar file. Having said which, we don't + // need to support this at all, so I'm storing it but ignoring it. + item.m_AdditionalExtensions = + readHandlerProperty(i, PropID::kAddExtension); + + std::string signature = readHandlerProperty(i, PropID::kSignature); + if (!signature.empty()) { + item.m_Signatures.push_back(signature); + if (m_MaxSignatureLen < signature.size()) { + m_MaxSignatureLen = signature.size(); + } + m_SignatureMap[signature] = item; + } + + std::string multiSig = readHandlerProperty(i, PropID::kMultiSignature); + const char* multiSigBytes = multiSig.c_str(); + std::size_t size = multiSig.length(); + while (size > 0) { + unsigned len = *multiSigBytes++; + size--; + if (len > size) + break; + std::string sig(multiSigBytes, multiSigBytes + len); + multiSigBytes = multiSigBytes + len; + size -= len; + item.m_Signatures.push_back(sig); + if (m_MaxSignatureLen < sig.size()) { + m_MaxSignatureLen = sig.size(); + } + m_SignatureMap[sig] = item; + } + + UInt32 offset = readHandlerProperty(i, PropID::kSignatureOffset); + item.m_SignatureOffset = offset; + + // Now split the extension up from the space separated string and create + // a map from each extension to the possible formats + // We could make these pointers but it's not a massive overhead and nobody + // should be changing this + std::wistringstream s(item.m_Extensions); + std::wstring t; + while (s >> t) { + m_FormatMap[t].push_back(item); + } + m_Formats.push_back(item); + } + return S_OK; +} + +ArchiveImpl::ArchiveImpl() + : m_Valid(false), m_LastError(Error::ERROR_NONE), m_Library("dlls/7zip.dll"), + m_PasswordCallback{} +{ + // Reset the log callback: + setLogCallback({}); + + if (!m_Library) { + m_LastError = Error::ERROR_LIBRARY_NOT_FOUND; + return; + } + + m_CreateObjectFunc = m_Library.resolve("CreateObject"); + if (m_CreateObjectFunc == nullptr) { + m_LastError = Error::ERROR_LIBRARY_INVALID; + return; + } + + m_GetHandlerPropertyFunc = m_Library.resolve("GetHandlerProperty2"); + if (m_GetHandlerPropertyFunc == nullptr) { + m_LastError = Error::ERROR_LIBRARY_INVALID; + return; + } + + try { + if (loadFormats() != S_OK) { + m_LastError = Error::ERROR_LIBRARY_INVALID; + return; + } + + m_Valid = true; + return; + } catch (std::exception const& e) { + m_LogCallback(LogLevel::Error, std::format(L"Caught exception {}.", e)); + m_LastError = Error::ERROR_LIBRARY_INVALID; + } +} + +ArchiveImpl::~ArchiveImpl() +{ + close(); +} + +bool ArchiveImpl::open(std::wstring const& archiveName, + PasswordCallback passwordCallback) +{ + m_ArchiveName = archiveName; // Just for debugging, not actually used... + + Formats formatList = m_Formats; + + // Convert to long path if it's not already: + std::filesystem::path filepath = IO::make_path(archiveName); + + // If it doesn't exist or is a directory, error + if (!exists(filepath) || is_directory(filepath)) { + m_LastError = Error::ERROR_ARCHIVE_NOT_FOUND; + return false; + } + + // in rars the password seems to be requested during extraction, not on open, so we + // need to hold on to the callback for now + m_PasswordCallback = passwordCallback; + + CComPtr file(new InputStream); + + if (!file->Open(filepath)) { + m_LastError = Error::ERROR_FAILED_TO_OPEN_ARCHIVE; + return false; + } + + CComPtr openCallbackPtr; + try { + openCallbackPtr = + new CArchiveOpenCallback(passwordCallback, m_LogCallback, filepath); + } catch (std::runtime_error const&) { + m_LastError = Error::ERROR_FAILED_TO_OPEN_ARCHIVE; + return false; + } + + // Try to open the archive + + bool sigMismatch = false; + + { + // Get the first iterator that is strictly > the signature we're looking for. + for (auto signatureInfo : m_SignatureMap) { + // Read the signature of the file and look that up. + std::vector buff; + buff.reserve(m_MaxSignatureLen); + UInt32 act; + file->Seek(0, STREAM_SEEK_SET, nullptr); + file->Read(buff.data(), static_cast(m_MaxSignatureLen), &act); + file->Seek(0, STREAM_SEEK_SET, nullptr); + std::string signature = std::string(buff.data(), act); + if (signatureInfo.first == std::string(buff.data(), signatureInfo.first.size())) { + if (m_CreateObjectFunc(&signatureInfo.second.m_ClassID, &IID_IInArchive, + (void**)&m_ArchivePtr) != S_OK) { + m_LastError = Error::ERROR_LIBRARY_ERROR; + return false; + } + + if (m_ArchivePtr->Open(file, 0, openCallbackPtr) != S_OK) { + m_LogCallback(LogLevel::Debug, + std::format(L"Failed to open {} using {} (from signature).", + archiveName, signatureInfo.second.m_Name)); + m_ArchivePtr.Release(); + } else { + m_LogCallback(LogLevel::Debug, + std::format(L"Opened {} using {} (from signature).", + archiveName, signatureInfo.second.m_Name)); + + // Retrieve the extension (warning: .extension() contains the dot): + std::wstring ext = + ArchiveStrings::towlower(filepath.extension().wstring().substr(1)); + std::wistringstream s(signatureInfo.second.m_Extensions); + std::wstring t; + bool found = false; + while (s >> t) { + if (t == ext) { + found = true; + break; + } + } + if (!found) { + m_LogCallback(LogLevel::Warning, + L"The extension of this file did not match the expected " + L"extensions for this format."); + sigMismatch = true; + } + } + // Arguably we should give up here if it's not OK if 7zip can't even start + // to decode even though we've found the format from the signature. + // Sadly, the 7zip API documentation is pretty well non-existant. + break; + } + std::vector::iterator iter = std::find_if( + formatList.begin(), formatList.end(), [=](ArchiveFormatInfo a) -> bool { + return a.m_Name == signatureInfo.second.m_Name; + }); + if (iter != formatList.end()) + formatList.erase(iter); + } + } + + { + // determine archive type based on extension + Formats const* formats = nullptr; + std::wstring ext = + ArchiveStrings::towlower(filepath.extension().wstring().substr(1)); + FormatMap::const_iterator map_iter = m_FormatMap.find(ext); + if (map_iter != m_FormatMap.end()) { + formats = &map_iter->second; + if (formats != nullptr) { + if (m_ArchivePtr == nullptr) { + // OK, we have some potential formats. If there is only one, try it now. If + // there are multiple formats, we'll try by signature lookup first. + for (ArchiveFormatInfo format : *formats) { + if (m_CreateObjectFunc(&format.m_ClassID, &IID_IInArchive, + (void**)&m_ArchivePtr) != S_OK) { + m_LastError = Error::ERROR_LIBRARY_ERROR; + return false; + } + + if (m_ArchivePtr->Open(file, 0, openCallbackPtr) != S_OK) { + m_LogCallback(LogLevel::Debug, + std::format(L"Failed to open {} using {} (from signature).", + archiveName, format.m_Name)); + m_ArchivePtr.Release(); + } else { + m_LogCallback(LogLevel::Debug, + std::format(L"Opened {} using {} (from signature).", + archiveName, format.m_Name)); + break; + } + + std::vector::iterator iter = std::find_if( + formatList.begin(), formatList.end(), [=](ArchiveFormatInfo a) -> bool { + return a.m_Name == format.m_Name; + }); + if (iter != formatList.end()) + formatList.erase(iter); + } + } else if (sigMismatch) { + std::vector vformats; + for (ArchiveFormatInfo format : *formats) { + vformats.push_back(format.m_Name); + } + m_LogCallback( + LogLevel::Warning, + std::format(L"The format(s) expected for this extension are: {}.", + ArchiveStrings::join(vformats, L", "))); + } + } + } + } + + if (m_ArchivePtr == nullptr) { + m_LogCallback(LogLevel::Warning, L"Trying to open an archive but could not " + L"recognize the extension or signature."); + m_LogCallback( + LogLevel::Debug, + L"Attempting to open the file with the remaining formats as a fallback..."); + for (auto format : formatList) { + if (m_CreateObjectFunc(&format.m_ClassID, &IID_IInArchive, + (void**)&m_ArchivePtr) != S_OK) { + m_LastError = Error::ERROR_LIBRARY_ERROR; + return false; + } + if (m_ArchivePtr->Open(file, 0, openCallbackPtr) == S_OK) { + m_LogCallback(LogLevel::Debug, + std::format(L"Opened {} using {} (from signature).", archiveName, + format.m_Name)); + m_LogCallback(LogLevel::Warning, + L"This archive likely has an incorrect extension."); + break; + } else + m_ArchivePtr.Release(); + } + } + + if (m_ArchivePtr == nullptr) { + m_LastError = Error::ERROR_INVALID_ARCHIVE_FORMAT; + return false; + } + + m_Password = openCallbackPtr->GetPassword(); + /* + UInt32 subFile = ULONG_MAX; + { + NCOM::CPropVariant prop; + if (m_ArchivePtr->GetArchiveProperty(kpidMainSubfile, &prop) != S_OK) { + throw std::runtime_error("failed to get property kpidMainSubfile"); + } + + if (prop.vt == VT_UI4) { + subFile = prop.ulVal; + } + } + + if (subFile != ULONG_MAX) { + std::wstring subPath = GetArchiveItemPath(m_ArchivePtr, subFile); + + CMyComPtr setSubArchiveName; + openCallbackPtr.QueryInterface(IID_IArchiveOpenSetSubArchiveName, (void + **)&setSubArchiveName); if (setSubArchiveName) { + setSubArchiveName->SetSubArchiveName(subPath.c_str()); + } + }*/ + + m_LastError = Error::ERROR_NONE; + + resetFileList(); + return true; +} + +void ArchiveImpl::close() +{ + if (m_ArchivePtr != nullptr) { + m_ArchivePtr->Close(); + } + clearFileList(); + m_ArchivePtr.Release(); + m_PasswordCallback = {}; +} + +void ArchiveImpl::clearFileList() +{ + for (std::vector::iterator iter = m_FileList.begin(); + iter != m_FileList.end(); ++iter) { + delete *iter; + } + m_FileList.clear(); +} + +void ArchiveImpl::resetFileList() +{ + UInt32 numItems = 0; + clearFileList(); + + m_ArchivePtr->GetNumberOfItems(&numItems); + + for (UInt32 i = 0; i < numItems; ++i) { + m_FileList.push_back(new FileDataImpl( + readProperty(i, kpidPath), readProperty(i, kpidSize), + readProperty(i, kpidCRC), readProperty(i, kpidIsDir))); + } +} + +bool ArchiveImpl::extract(std::wstring const& outputDirectory, + ProgressCallback progressCallback, + FileChangeCallback fileChangeCallback, + ErrorCallback errorCallback) + +{ + // Retrieve the list of indices we want to extract: + std::vector indices; + UInt64 totalSize = 0; + for (std::size_t i = 0; i < m_FileList.size(); ++i) { + FileDataImpl* fileData = static_cast(m_FileList[i]); + if (!fileData->isEmpty()) { + indices.push_back(static_cast(i)); + totalSize += fileData->getSize(); + } + } + + m_ExtractCallback = new CArchiveExtractCallback( + progressCallback, fileChangeCallback, errorCallback, m_PasswordCallback, + m_LogCallback, m_ArchivePtr, outputDirectory, &m_FileList[0], m_FileList.size(), + totalSize, &m_Password); + HRESULT result = m_ArchivePtr->Extract( + indices.data(), static_cast(indices.size()), false, m_ExtractCallback); + // Note: m_ExtractCallBack is deleted by Extract + switch (result) { + case S_OK: { + // nop + } break; + case E_ABORT: { + m_LastError = Error::ERROR_EXTRACT_CANCELLED; + } break; + case E_OUTOFMEMORY: { + m_LastError = Error::ERROR_OUT_OF_MEMORY; + } break; + default: { + m_LastError = Error::ERROR_LIBRARY_ERROR; + } break; + } + + return result == S_OK; +} + +void ArchiveImpl::cancel() +{ + m_ExtractCallback->SetCanceled(true); +} + +std::unique_ptr CreateArchive() +{ + return std::make_unique(); +} diff --git a/libs/archive/src/compat.h b/libs/archive/src/compat.h new file mode 100644 index 0000000..65c2ec5 --- /dev/null +++ b/libs/archive/src/compat.h @@ -0,0 +1,87 @@ +/* +Mod Organizer archive handling - Linux compatibility header + +Copyright (C) 2024 MO2 Team. All rights reserved. + +This header provides Windows COM compatibility types for Linux builds. +On Windows, the real Windows headers are used instead. +*/ + +#ifndef ARCHIVE_COMPAT_H +#define ARCHIVE_COMPAT_H + +#ifdef _WIN32 + +#include +#include +#include +#include +#include + +#else // Linux + +#include +#include + +// PropVariantInit / PropVariantClear - map to VariantClear from 7zip SDK +inline HRESULT PropVariantInit(PROPVARIANT* pvar) { + pvar->vt = VT_EMPTY; + return S_OK; +} + +inline HRESULT PropVariantClear(PROPVARIANT* pvar) { + return VariantClear(pvar); +} + +// A minimal CComPtr replacement for Linux +template +class CComPtr +{ +public: + CComPtr() : p(nullptr) {} + CComPtr(T* lp) : p(lp) { if (p) p->AddRef(); } + CComPtr(const CComPtr& other) : p(other.p) { if (p) p->AddRef(); } + ~CComPtr() { Release(); } + + CComPtr& operator=(T* lp) { + if (lp) lp->AddRef(); + Release(); + p = lp; + return *this; + } + + CComPtr& operator=(const CComPtr& other) { + if (this != &other) { + if (other.p) other.p->AddRef(); + Release(); + p = other.p; + } + return *this; + } + + void Release() { + if (p) { + p->Release(); + p = nullptr; + } + } + + T* Detach() { + T* pt = p; + p = nullptr; + return pt; + } + + operator T*() const { return p; } + T* operator->() const { return p; } + T** operator&() { return &p; } + bool operator!() const { return p == nullptr; } + bool operator==(std::nullptr_t) const { return p == nullptr; } + bool operator!=(std::nullptr_t) const { return p != nullptr; } + + T* p; +}; + +#endif // _WIN32 + +#endif // ARCHIVE_COMPAT_H diff --git a/libs/archive/src/extractcallback.cpp b/libs/archive/src/extractcallback.cpp new file mode 100644 index 0000000..7629298 --- /dev/null +++ b/libs/archive/src/extractcallback.cpp @@ -0,0 +1,364 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 "compat.h" + +#include +#include +#include +#include +#include + +#include "archive.h" +#include "extractcallback.h" +#include "propertyvariant.h" + +std::wstring operationResultToString(Int32 operationResult) +{ + namespace R = NArchive::NExtract::NOperationResult; + + switch (operationResult) { + case R::kOK: + return {}; + + case R::kUnsupportedMethod: + return L"Encoding method unsupported"; + + case R::kDataError: + return L"Data error"; + + case R::kCRCError: + return L"CRC error"; + + case R::kUnavailable: + return L"Unavailable"; + + case R::kUnexpectedEnd: + return L"Unexpected end of archive"; + + case R::kDataAfterEnd: + return L"Data after end of archive"; + + case R::kIsNotArc: + return L"Not an ARC"; + + case R::kHeadersError: + return L"Bad headers"; + + case R::kWrongPassword: + return L"Wrong password"; + + default: + return std::format(L"Unknown error {}", operationResult); + } +} + +CArchiveExtractCallback::CArchiveExtractCallback( + Archive::ProgressCallback progressCallback, + Archive::FileChangeCallback fileChangeCallback, + Archive::ErrorCallback errorCallback, Archive::PasswordCallback passwordCallback, + Archive::LogCallback logCallback, IInArchive* archiveHandler, + std::wstring const& directoryPath, FileData* const* fileData, std::size_t nbFiles, + UInt64 totalFileSize, std::wstring* password) + : m_ArchiveHandler(archiveHandler), m_Total(0), m_DirectoryPath(), + m_Extracting(false), m_Canceled(false), m_Timers{}, m_ProcessedFileInfo{}, + m_OutputFileStream{}, m_OutFileStreamCom{}, m_FileData(fileData), + m_NbFiles(nbFiles), m_TotalFileSize(totalFileSize), m_LastCallbackFileSize(0), + m_ExtractedFileSize(0), m_ProgressCallback(progressCallback), + m_FileChangeCallback(fileChangeCallback), m_ErrorCallback(errorCallback), + m_PasswordCallback(passwordCallback), m_LogCallback(logCallback), + m_Password(password) +{ + m_DirectoryPath = IO::make_path(directoryPath); +} + +CArchiveExtractCallback::~CArchiveExtractCallback() +{ +#ifdef INSTRUMENT_ARCHIVE + m_LogCallback(Archive::LogLevel::Debug, m_Timers.GetStream.toString(L"GetStream")); + m_LogCallback(Archive::LogLevel::Debug, m_Timers.SetOperationResult.SetMTime.toString( + L"SetOperationResult.SetMTime")); + m_LogCallback(Archive::LogLevel::Debug, m_Timers.SetOperationResult.Close.toString( + L"SetOperationResult.Close")); + m_LogCallback(Archive::LogLevel::Debug, m_Timers.SetOperationResult.Release.toString( + L"SetOperationResult.Release")); + m_LogCallback(Archive::LogLevel::Debug, + m_Timers.SetOperationResult.SetFileAttributesW.toString( + L"SetOperationResult.SetFileAttributesW")); +#endif +} + +STDMETHODIMP CArchiveExtractCallback::SetTotal(UInt64 size) throw() +{ + m_Total = size; + return S_OK; +} + +STDMETHODIMP CArchiveExtractCallback::SetCompleted(const UInt64* completed) throw() +{ + if (m_ProgressCallback) { + m_ProgressCallback(Archive::ProgressType::ARCHIVE, *completed, m_Total); + } + return m_Canceled ? E_ABORT : S_OK; +} + +template +bool CArchiveExtractCallback::getOptionalProperty(UInt32 index, int property, + T* result) const +{ + PropertyVariant prop; + if (m_ArchiveHandler->GetProperty(index, property, &prop) != S_OK) { + m_LogCallback(Archive::LogLevel::Error, + std::format(L"Error getting property {}.", property)); + return false; + } + if (prop.is_empty()) { + return false; + } + *result = static_cast(prop); + return true; +} + +template +bool CArchiveExtractCallback::getProperty(UInt32 index, int property, T* result) const +{ + PropertyVariant prop; + if (m_ArchiveHandler->GetProperty(index, property, &prop) != S_OK) { + m_LogCallback(Archive::LogLevel::Error, + std::format(L"Error getting property {}.", property)); + return false; + } + + *result = static_cast(prop); + return true; +} + +STDMETHODIMP CArchiveExtractCallback::GetStream(UInt32 index, + ISequentialOutStream** outStream, + Int32 askExtractMode) throw() +{ + [[maybe_unused]] auto guard = m_Timers.GetStream.instrument(); + namespace fs = std::filesystem; + + *outStream = nullptr; + m_OutFileStreamCom.Release(); + + m_FullProcessedPaths.clear(); + m_Extracting = false; + + if (askExtractMode != NArchive::NExtract::NAskMode::kExtract) { + return S_OK; + } + + std::vector filenames = m_FileData[index]->getOutputFilePaths(); + m_FileData[index]->clearOutputFilePaths(); + if (filenames.empty()) { + return S_OK; + } + +#ifndef _WIN32 + // Archives from Windows contain backslash path separators which are valid + // filename characters on Linux - convert them to forward slashes. + for (auto& fn : filenames) { + std::replace(fn.begin(), fn.end(), L'\\', L'/'); + } +#endif + + try { + m_ProcessedFileInfo.AttribDefined = + getOptionalProperty(index, kpidAttrib, &m_ProcessedFileInfo.Attrib); + + if (!getProperty(index, kpidIsDir, &m_ProcessedFileInfo.isDir)) { + return E_ABORT; + } + + // Why do we do this? And if we are doing this, shouldn't we copy the created + // and accessed times (kpidATime, kpidCTime) as well? + m_ProcessedFileInfo.MTimeDefined = + getOptionalProperty(index, kpidMTime, &m_ProcessedFileInfo.MTime); + + if (m_ProcessedFileInfo.isDir) { + for (auto const& filename : filenames) { + auto fullpath = m_DirectoryPath / fs::path(filename).make_preferred(); + std::error_code ec; + std::filesystem::create_directories(fullpath, ec); + if (ec) { + reportError(L"cannot created directory '{}': {}", fullpath, ec); + return E_ABORT; + } + m_FullProcessedPaths.push_back(fullpath); + } + } else { + for (auto const& filename : filenames) { + auto fullProcessedPath = m_DirectoryPath / fs::path(filename).make_preferred(); + // If the filename contains a '/' we want to make the directory + auto directoryPath = fullProcessedPath.parent_path(); + if (!fs::exists(directoryPath)) { + // Make the containing directory + std::error_code ec; + std::filesystem::create_directories(directoryPath, ec); + if (ec) { + reportError(L"cannot created directory '{}': {}", directoryPath, ec); + return E_ABORT; + } + // m_DirectoryPath.mkpath(filename.left(slashPos)); + } + // If the file already exists, delete it + if (fs::exists(fullProcessedPath)) { + std::error_code ec; + if (!fs::remove(fullProcessedPath, ec)) { + reportError(L"cannot delete output file '{}': {}", fullProcessedPath, ec); + return E_ABORT; + } + } + m_FullProcessedPaths.push_back(fullProcessedPath); + } + + m_OutputFileStream = new MultiOutputStream([this](UInt32 size, UInt64) { + m_ExtractedFileSize += size; + if (m_ProgressCallback) { + m_ProgressCallback(Archive::ProgressType::EXTRACTION, m_ExtractedFileSize, + m_TotalFileSize); + } + }); + CComPtr outStreamCom(m_OutputFileStream); + + if (!m_OutputFileStream->Open(m_FullProcessedPaths)) { + reportError(L"cannot open output file '{}': {}", m_FullProcessedPaths[0], + ::GetLastError()); + return E_ABORT; + } + + UInt64 fileSize; + auto fileSizeFound = getOptionalProperty(index, kpidSize, &fileSize); + if (fileSizeFound && m_OutputFileStream->SetSize(fileSize) != S_OK) { + m_LogCallback(Archive::LogLevel::Error, + std::format(L"SetSize() failed on {}.", m_FullProcessedPaths[0])); + } + + // This is messy but I can't find another way of doing it. A simple + // assignment of m_outFileStream to *outStream doesn't increase the + // reference count. + m_OutFileStreamCom = outStreamCom; + *outStream = outStreamCom.Detach(); + } + + if (m_FileChangeCallback) { + m_FileChangeCallback(Archive::FileChangeType::EXTRACTION_START, filenames[0]); + } + + return S_OK; + } catch (std::exception const& e) { + m_LogCallback(Archive::LogLevel::Error, + std::format(L"Caught exception {} in GetStream.", e)); + } + return E_FAIL; +} + +STDMETHODIMP CArchiveExtractCallback::PrepareOperation(Int32 askExtractMode) throw() +{ + if (m_Canceled) { + return E_ABORT; + } + m_Extracting = askExtractMode == NArchive::NExtract::NAskMode::kExtract; + return S_OK; +} + +STDMETHODIMP CArchiveExtractCallback::SetOperationResult(Int32 operationResult) throw() +{ + if (operationResult != NArchive::NExtract::NOperationResult::kOK) { + reportError(operationResultToString(operationResult)); + } + + if (m_OutFileStreamCom) { + if (m_ProcessedFileInfo.MTimeDefined) { + [[maybe_unused]] auto guard = m_Timers.SetOperationResult.SetMTime.instrument(); + m_OutputFileStream->SetMTime(&m_ProcessedFileInfo.MTime); + } + [[maybe_unused]] auto guard = m_Timers.SetOperationResult.Close.instrument(); + RINOK(m_OutputFileStream->Close()) + } + + { + [[maybe_unused]] auto guard = m_Timers.SetOperationResult.Release.instrument(); + m_OutFileStreamCom.Release(); + } + + [[maybe_unused]] auto guard2 = m_Timers.SetOperationResult.SetFileAttributesW.instrument(); + if (m_Extracting && m_ProcessedFileInfo.AttribDefined) { + // this is moderately annoying. I can't do this on the file handle because if + // the file in question is a directory there isn't a file handle. + // Also I'd like to convert the attributes to QT attributes but I'm not sure + // if that's possible. Hence the conversions and strange string. + for (auto& path : m_FullProcessedPaths) { +#ifdef _WIN32 + std::wstring const fn = L"\\\\?\\" + path.native(); + // If the attributes are POSIX-based, fix that + if (m_ProcessedFileInfo.Attrib & 0xF0000000) + m_ProcessedFileInfo.Attrib &= 0x7FFF; + + // Should probably log any errors here somehow + ::SetFileAttributesW(fn.c_str(), m_ProcessedFileInfo.Attrib); +#else + // On Linux, we could set file permissions based on the attributes, + // but Windows file attributes don't map well to POSIX permissions. + // For now, we only handle the read-only attribute and only for files. + // Applying read-only to directories can break extraction when later + // files need to be created inside those directories. + if (!m_ProcessedFileInfo.isDir && + (m_ProcessedFileInfo.Attrib & FILE_ATTRIBUTE_READONLY)) { + std::filesystem::permissions(path, + std::filesystem::perms::owner_write, + std::filesystem::perm_options::remove); + } else if (m_ProcessedFileInfo.isDir) { + // Keep extracted directories writable for the owner. + std::filesystem::permissions(path, + std::filesystem::perms::owner_write, + std::filesystem::perm_options::add); + } +#endif + } + } + + return S_OK; +} + +STDMETHODIMP CArchiveExtractCallback::CryptoGetTextPassword(BSTR* passwordOut) throw() +{ + // if we've already got a password, don't ask again (and again...) + if (m_Password->empty() && m_PasswordCallback) { + *m_Password = m_PasswordCallback(); + } + + *passwordOut = ::SysAllocString(m_Password->c_str()); + return *passwordOut != 0 ? S_OK : E_OUTOFMEMORY; +} + +void CArchiveExtractCallback::SetCanceled(bool aCanceled) +{ + m_Canceled = aCanceled; +} + +void CArchiveExtractCallback::reportError(std::wstring const& message) +{ + if (m_ErrorCallback) { + m_ErrorCallback(message); + } +} diff --git a/libs/archive/src/extractcallback.h b/libs/archive/src/extractcallback.h new file mode 100644 index 0000000..f95fc86 --- /dev/null +++ b/libs/archive/src/extractcallback.h @@ -0,0 +1,133 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 EXTRACTCALLBACK_H +#define EXTRACTCALLBACK_H + +#include +#include +#include +#include + +#include <7zip/Archive/IArchive.h> +#include <7zip/IPassword.h> + +#include "compat.h" + +#include "archive.h" +#include "formatter.h" +#include "instrument.h" +#include "multioutputstream.h" +#include "unknown_impl.h" + +class FileData; + +class CArchiveExtractCallback : public IArchiveExtractCallback, + public ICryptoGetTextPassword +{ + + // A note: It appears that the IArchiveExtractCallback interface includes the + // IProgress interface, swo we need to respond to it + UNKNOWN_3_INTERFACE(IArchiveExtractCallback, ICryptoGetTextPassword, IProgress); + +public: + CArchiveExtractCallback(Archive::ProgressCallback progressCallback, + Archive::FileChangeCallback fileChangeCallback, + Archive::ErrorCallback errorCallback, + Archive::PasswordCallback passwordCallback, + Archive::LogCallback logCallback, IInArchive* archiveHandler, + std::wstring const& directoryPath, FileData* const* fileData, + std::size_t nbFiles, UInt64 totalFileSize, + std::wstring* password); + + virtual ~CArchiveExtractCallback(); + + void SetCanceled(bool aCanceled); + + Z7_IFACE_COM7_IMP(IProgress) + Z7_IFACE_COM7_IMP(IArchiveExtractCallback) + + // ICryptoGetTextPassword + STDMETHOD(CryptoGetTextPassword)(BSTR* aPassword) throw(); + +private: + void reportError(const std::wstring& message); + + template + void reportError(std::wformat_string format, Args&&... args) + { + reportError(std::format(format, std::forward(args)...)); + } + + template + bool getOptionalProperty(UInt32 index, int property, T* result) const; + template + bool getProperty(UInt32 index, int property, T* result) const; + +private: + CComPtr m_ArchiveHandler; + + UInt64 m_Total; + + std::filesystem::path m_DirectoryPath; + bool m_Extracting; + std::atomic m_Canceled; + + struct + { + ArchiveTimers::Timer GetStream; + struct + { + ArchiveTimers::Timer SetMTime; + ArchiveTimers::Timer Close; + ArchiveTimers::Timer Release; + ArchiveTimers::Timer SetFileAttributesW; + } SetOperationResult; + } m_Timers; + + struct CProcessedFileInfo + { + FILETIME MTime; + UInt32 Attrib; + bool isDir; + bool AttribDefined; + bool MTimeDefined; + } m_ProcessedFileInfo; + + MultiOutputStream* m_OutputFileStream; + CComPtr m_OutFileStreamCom; + + std::vector m_FullProcessedPaths; + + FileData* const* m_FileData; + std::size_t m_NbFiles; + UInt64 m_TotalFileSize; + UInt64 m_LastCallbackFileSize; + UInt64 m_ExtractedFileSize; + + Archive::ProgressCallback m_ProgressCallback; + Archive::FileChangeCallback m_FileChangeCallback; + Archive::ErrorCallback m_ErrorCallback; + Archive::PasswordCallback m_PasswordCallback; + Archive::LogCallback m_LogCallback; + std::wstring* m_Password; +}; + +#endif // EXTRACTCALLBACK_H diff --git a/libs/archive/src/fileio.cpp b/libs/archive/src/fileio.cpp new file mode 100644 index 0000000..83d0665 --- /dev/null +++ b/libs/archive/src/fileio.cpp @@ -0,0 +1,449 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2020 MO2 Team. 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 "fileio.h" + +#ifdef _WIN32 + +inline bool BOOLToBool(BOOL v) +{ + return (v != FALSE); +} + +namespace IO +{ + +// FileBase + +bool FileBase::Close() noexcept +{ + if (m_Handle == INVALID_HANDLE_VALUE) + return true; + if (!::CloseHandle(m_Handle)) + return false; + m_Handle = INVALID_HANDLE_VALUE; + return true; +} + +bool FileBase::GetPosition(UInt64& position) noexcept +{ + return Seek(0, FILE_CURRENT, position); +} + +bool FileBase::GetLength(UInt64& length) const noexcept +{ + DWORD sizeHigh; + DWORD sizeLow = ::GetFileSize(m_Handle, &sizeHigh); + if (sizeLow == 0xFFFFFFFF) + if (::GetLastError() != NO_ERROR) + return false; + length = (((UInt64)sizeHigh) << 32) + sizeLow; + return true; +} + +bool FileBase::Seek(Int64 distanceToMove, DWORD moveMethod, + UInt64& newPosition) noexcept +{ + LONG high = (LONG)(distanceToMove >> 32); + DWORD low = ::SetFilePointer(m_Handle, (LONG)(distanceToMove & 0xFFFFFFFF), &high, + moveMethod); + if (low == 0xFFFFFFFF) + if (::GetLastError() != NO_ERROR) + return false; + newPosition = (((UInt64)(UInt32)high) << 32) + low; + return true; +} +bool FileBase::Seek(UInt64 position, UInt64& newPosition) noexcept +{ + return Seek(position, FILE_BEGIN, newPosition); +} + +bool FileBase::SeekToBegin() noexcept +{ + UInt64 newPosition; + return Seek(0, newPosition); +} + +bool FileBase::SeekToEnd(UInt64& newPosition) noexcept +{ + return Seek(0, FILE_END, newPosition); +} + +bool FileBase::Create(std::filesystem::path const& path, DWORD desiredAccess, + DWORD shareMode, DWORD creationDisposition, + DWORD flagsAndAttributes) noexcept +{ + if (!Close()) { + return false; + } + + m_Handle = + ::CreateFileW(path.c_str(), desiredAccess, shareMode, (LPSECURITY_ATTRIBUTES)NULL, + creationDisposition, flagsAndAttributes, (HANDLE)NULL); + + return m_Handle != INVALID_HANDLE_VALUE; +} + +bool FileBase::GetFileInformation(std::filesystem::path const& path, + FileInfo* info) noexcept +{ + // Use FileBase to open/close the file: + FileBase file; + if (!file.Create(path, 0, FILE_SHARE_READ, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS)) + return false; + + BY_HANDLE_FILE_INFORMATION finfo; + if (!BOOLToBool(GetFileInformationByHandle(file.m_Handle, &finfo))) { + return false; + } + + *info = FileInfo(path, finfo); + return true; +} + +// FileIn + +bool FileIn::Open(std::filesystem::path const& filepath, DWORD shareMode, + DWORD creationDisposition, DWORD flagsAndAttributes) noexcept +{ + bool res = Create(filepath.c_str(), GENERIC_READ, shareMode, creationDisposition, + flagsAndAttributes); + return res; +} +bool FileIn::OpenShared(std::filesystem::path const& filepath, + bool shareForWrite) noexcept +{ + return Open(filepath, FILE_SHARE_READ | (shareForWrite ? FILE_SHARE_WRITE : 0), + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL); +} +bool FileIn::Open(std::filesystem::path const& filepath) noexcept +{ + return OpenShared(filepath, false); +} +bool FileIn::Read(void* data, UInt32 size, UInt32& processedSize) noexcept +{ + processedSize = 0; + do { + UInt32 processedLoc = 0; + bool res = ReadPart(data, size, processedLoc); + processedSize += processedLoc; + if (!res) + return false; + if (processedLoc == 0) + return true; + data = (void*)((unsigned char*)data + processedLoc); + size -= processedLoc; + } while (size > 0); + return true; +} +bool FileIn::Read1(void* data, UInt32 size, UInt32& processedSize) noexcept +{ + DWORD processedLoc = 0; + bool res = BOOLToBool(::ReadFile(m_Handle, data, size, &processedLoc, NULL)); + processedSize = (UInt32)processedLoc; + return res; +} +bool FileIn::ReadPart(void* data, UInt32 size, UInt32& processedSize) noexcept +{ + if (size > kChunkSizeMax) + size = kChunkSizeMax; + return Read1(data, size, processedSize); +} + +// FileOut + +bool FileOut::Open(std::filesystem::path const& fileName, DWORD shareMode, + DWORD creationDisposition, DWORD flagsAndAttributes) noexcept +{ + return Create(fileName, GENERIC_WRITE, shareMode, creationDisposition, + flagsAndAttributes); +} + +bool FileOut::Open(std::filesystem::path const& fileName) noexcept +{ + return Open(fileName, FILE_SHARE_READ, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL); +} + +bool FileOut::SetTime(const FILETIME* cTime, const FILETIME* aTime, + const FILETIME* mTime) noexcept +{ + return BOOLToBool(::SetFileTime(m_Handle, cTime, aTime, mTime)); +} +bool FileOut::SetMTime(const FILETIME* mTime) noexcept +{ + return SetTime(NULL, NULL, mTime); +} +bool FileOut::Write(const void* data, UInt32 size, UInt32& processedSize) noexcept +{ + processedSize = 0; + do { + UInt32 processedLoc = 0; + bool res = WritePart(data, size, processedLoc); + processedSize += processedLoc; + if (!res) + return false; + if (processedLoc == 0) + return true; + data = (const void*)((const unsigned char*)data + processedLoc); + size -= processedLoc; + } while (size > 0); + return true; +} + +bool FileOut::SetLength(UInt64 length) noexcept +{ + UInt64 newPosition; + if (!Seek(length, newPosition)) + return false; + if (newPosition != length) + return false; + return SetEndOfFile(); +} +bool FileOut::SetEndOfFile() noexcept +{ + return BOOLToBool(::SetEndOfFile(m_Handle)); +} + +bool FileOut::WritePart(const void* data, UInt32 size, UInt32& processedSize) noexcept +{ + if (size > kChunkSizeMax) + size = kChunkSizeMax; + DWORD processedLoc = 0; + bool res = BOOLToBool(::WriteFile(m_Handle, data, size, &processedLoc, NULL)); + processedSize = (UInt32)processedLoc; + return res; +} + +} // namespace IO + +#else // Linux + +#include +#include +#include +#include +#include +#include + +namespace IO +{ + +// FileBase + +bool FileBase::Close() noexcept +{ + if (m_Fd == -1) + return true; + if (::close(m_Fd) != 0) + return false; + m_Fd = -1; + return true; +} + +bool FileBase::GetPosition(UInt64& position) noexcept +{ + return Seek(0, SEEK_CUR, position); +} + +bool FileBase::GetLength(UInt64& length) const noexcept +{ + struct stat st; + if (fstat(m_Fd, &st) != 0) + return false; + length = (UInt64)st.st_size; + return true; +} + +bool FileBase::Seek(Int64 distanceToMove, int whence, UInt64& newPosition) noexcept +{ + off_t result = ::lseek(m_Fd, (off_t)distanceToMove, whence); + if (result == (off_t)-1) + return false; + newPosition = (UInt64)result; + return true; +} + +bool FileBase::Seek(UInt64 position, UInt64& newPosition) noexcept +{ + return Seek((Int64)position, SEEK_SET, newPosition); +} + +bool FileBase::SeekToBegin() noexcept +{ + UInt64 newPosition; + return Seek(0, newPosition); +} + +bool FileBase::SeekToEnd(UInt64& newPosition) noexcept +{ + return Seek(0, SEEK_END, newPosition); +} + +bool FileBase::Create(std::filesystem::path const& path, int flags, int mode) noexcept +{ + if (!Close()) { + return false; + } + + m_Fd = ::open(path.c_str(), flags, mode); + return m_Fd != -1; +} + +bool FileBase::GetFileInformation(std::filesystem::path const& path, + FileInfo* info) noexcept +{ + struct stat st; + if (::stat(path.c_str(), &st) != 0) { + return false; + } + + *info = FileInfo(path, st); + return true; +} + +// FileIn + +bool FileIn::Open(std::filesystem::path const& filepath) noexcept +{ + return Create(filepath, O_RDONLY); +} + +bool FileIn::Read(void* data, UInt32 size, UInt32& processedSize) noexcept +{ + processedSize = 0; + do { + UInt32 processedLoc = 0; + bool res = ReadPart(data, size, processedLoc); + processedSize += processedLoc; + if (!res) + return false; + if (processedLoc == 0) + return true; + data = (void*)((unsigned char*)data + processedLoc); + size -= processedLoc; + } while (size > 0); + return true; +} + +bool FileIn::Read1(void* data, UInt32 size, UInt32& processedSize) noexcept +{ + ssize_t result = ::read(m_Fd, data, size); + if (result < 0) { + processedSize = 0; + return false; + } + processedSize = (UInt32)result; + return true; +} + +bool FileIn::ReadPart(void* data, UInt32 size, UInt32& processedSize) noexcept +{ + if (size > kChunkSizeMax) + size = kChunkSizeMax; + return Read1(data, size, processedSize); +} + +// FileOut + +bool FileOut::Open(std::filesystem::path const& fileName) noexcept +{ + return Create(fileName, O_WRONLY | O_CREAT | O_TRUNC, 0644); +} + +bool FileOut::SetTime(const FILETIME* /*cTime*/, const FILETIME* /*aTime*/, + const FILETIME* mTime) noexcept +{ + return SetMTime(mTime); +} + +bool FileOut::SetMTime(const FILETIME* mTime) noexcept +{ + if (!mTime) return true; + + // Convert FILETIME to timespec + // FILETIME is 100ns intervals since 1601-01-01 + // Unix time is seconds since 1970-01-01 + constexpr UInt64 EPOCH_DIFF = 116444736000000000ULL; + UInt64 ticks = ((UInt64)mTime->dwHighDateTime << 32) | mTime->dwLowDateTime; + if (ticks < EPOCH_DIFF) return false; + ticks -= EPOCH_DIFF; + + struct timespec times[2]; + // atime - keep current + times[0].tv_sec = 0; + times[0].tv_nsec = UTIME_OMIT; + // mtime + times[1].tv_sec = (time_t)(ticks / 10000000ULL); + times[1].tv_nsec = (long)((ticks % 10000000ULL) * 100); + + return futimens(m_Fd, times) == 0; +} + +bool FileOut::Write(const void* data, UInt32 size, UInt32& processedSize) noexcept +{ + processedSize = 0; + do { + UInt32 processedLoc = 0; + bool res = WritePart(data, size, processedLoc); + processedSize += processedLoc; + if (!res) + return false; + if (processedLoc == 0) + return true; + data = (const void*)((const unsigned char*)data + processedLoc); + size -= processedLoc; + } while (size > 0); + return true; +} + +bool FileOut::SetLength(UInt64 length) noexcept +{ + UInt64 newPosition; + if (!Seek(length, newPosition)) + return false; + if (newPosition != length) + return false; + return SetEndOfFile(); +} + +bool FileOut::SetEndOfFile() noexcept +{ + UInt64 pos; + if (!GetPosition(pos)) + return false; + return ftruncate(m_Fd, (off_t)pos) == 0; +} + +bool FileOut::WritePart(const void* data, UInt32 size, UInt32& processedSize) noexcept +{ + if (size > kChunkSizeMax) + size = kChunkSizeMax; + ssize_t result = ::write(m_Fd, data, size); + if (result < 0) { + processedSize = 0; + return false; + } + processedSize = (UInt32)result; + return true; +} + +} // namespace IO + +#endif diff --git a/libs/archive/src/fileio.h b/libs/archive/src/fileio.h new file mode 100644 index 0000000..72b71c7 --- /dev/null +++ b/libs/archive/src/fileio.h @@ -0,0 +1,343 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2020 MO2 Team. 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 ARCHIVE_FILEIO_H +#define ARCHIVE_FILEIO_H + +// This code is adapted from 7z client code. + +#ifdef _WIN32 +#include +#include "7zip//Archive/IArchive.h" +#else +#include +#include <7zip/Archive/IArchive.h> +#endif + +#include +#include + +#ifndef _WIN32 +#include +#endif + +namespace IO +{ + +#ifdef _WIN32 + +/** + * Small class that wraps windows BY_HANDLE_FILE_INFORMATION and returns + * type matching 7z types. + */ +class FileInfo +{ +public: + FileInfo() : m_Valid{false} {}; + FileInfo(std::filesystem::path const& path, BY_HANDLE_FILE_INFORMATION fileInfo) + : m_Valid{true}, m_Path(path), m_FileInfo{fileInfo} + {} + + bool isValid() const { return m_Valid; } + + const std::filesystem::path& path() const { return m_Path; } + + UInt32 fileAttributes() const { return m_FileInfo.dwFileAttributes; } + FILETIME creationTime() const { return m_FileInfo.ftCreationTime; } + FILETIME lastAccessTime() const { return m_FileInfo.ftLastAccessTime; } + FILETIME lastWriteTime() const { return m_FileInfo.ftLastWriteTime; } + UInt32 volumeSerialNumber() const { return m_FileInfo.dwVolumeSerialNumber; } + UInt64 fileSize() const + { + return ((UInt64)m_FileInfo.nFileSizeHigh) << 32 | m_FileInfo.nFileSizeLow; + } + UInt32 numberOfLinks() const { return m_FileInfo.nNumberOfLinks; } + UInt64 fileInfex() const + { + return ((UInt64)m_FileInfo.nFileIndexHigh) << 32 | m_FileInfo.nFileIndexLow; + } + + bool isArchived() const { return MatchesMask(FILE_ATTRIBUTE_ARCHIVE); } + bool isCompressed() const { return MatchesMask(FILE_ATTRIBUTE_COMPRESSED); } + bool isDir() const { return MatchesMask(FILE_ATTRIBUTE_DIRECTORY); } + bool isEncrypted() const { return MatchesMask(FILE_ATTRIBUTE_ENCRYPTED); } + bool isHidden() const { return MatchesMask(FILE_ATTRIBUTE_HIDDEN); } + bool isNormal() const { return MatchesMask(FILE_ATTRIBUTE_NORMAL); } + bool isOffline() const { return MatchesMask(FILE_ATTRIBUTE_OFFLINE); } + bool isReadOnly() const { return MatchesMask(FILE_ATTRIBUTE_READONLY); } + bool iasReparsePoint() const { return MatchesMask(FILE_ATTRIBUTE_REPARSE_POINT); } + bool isSparse() const { return MatchesMask(FILE_ATTRIBUTE_SPARSE_FILE); } + bool isSystem() const { return MatchesMask(FILE_ATTRIBUTE_SYSTEM); } + bool isTemporary() const { return MatchesMask(FILE_ATTRIBUTE_TEMPORARY); } + +private: + bool MatchesMask(UINT32 mask) const + { + return ((m_FileInfo.dwFileAttributes & mask) != 0); + } + + bool m_Valid; + std::filesystem::path m_Path; + BY_HANDLE_FILE_INFORMATION m_FileInfo; +}; + +#else // Linux + +/** + * FileInfo class for Linux - uses stat() to get file information. + * Returns 7zip-compatible types. + */ +class FileInfo +{ +public: + FileInfo() : m_Valid{false}, m_Stat{} {}; + FileInfo(std::filesystem::path const& path, struct stat const& st) + : m_Valid{true}, m_Path(path), m_Stat{st} + {} + + bool isValid() const { return m_Valid; } + + const std::filesystem::path& path() const { return m_Path; } + + UInt32 fileAttributes() const { + UInt32 attr = 0; + if (S_ISDIR(m_Stat.st_mode)) attr |= FILE_ATTRIBUTE_DIRECTORY; + if (!(m_Stat.st_mode & S_IWUSR)) attr |= FILE_ATTRIBUTE_READONLY; + if (attr == 0) attr = FILE_ATTRIBUTE_NORMAL; + return attr; + } + + // Convert timespec to FILETIME (100ns intervals since 1601-01-01) + static FILETIME timespecToFiletime(struct timespec const& ts) { + // Offset between 1601-01-01 and 1970-01-01 in 100ns intervals + constexpr UInt64 EPOCH_DIFF = 116444736000000000ULL; + UInt64 ticks = (UInt64)ts.tv_sec * 10000000ULL + (UInt64)ts.tv_nsec / 100ULL + EPOCH_DIFF; + FILETIME ft; + ft.dwLowDateTime = (DWORD)(ticks & 0xFFFFFFFF); + ft.dwHighDateTime = (DWORD)(ticks >> 32); + return ft; + } + + FILETIME creationTime() const { + // Linux doesn't have creation time in all filesystems, use ctime (status change) + return timespecToFiletime(m_Stat.st_ctim); + } + FILETIME lastAccessTime() const { + return timespecToFiletime(m_Stat.st_atim); + } + FILETIME lastWriteTime() const { + return timespecToFiletime(m_Stat.st_mtim); + } + UInt32 volumeSerialNumber() const { return (UInt32)m_Stat.st_dev; } + UInt64 fileSize() const { return (UInt64)m_Stat.st_size; } + UInt32 numberOfLinks() const { return (UInt32)m_Stat.st_nlink; } + UInt64 fileInfex() const { return (UInt64)m_Stat.st_ino; } + + bool isArchived() const { return false; } + bool isCompressed() const { return false; } + bool isDir() const { return S_ISDIR(m_Stat.st_mode); } + bool isEncrypted() const { return false; } + bool isHidden() const { return false; } + bool isNormal() const { return S_ISREG(m_Stat.st_mode); } + bool isOffline() const { return false; } + bool isReadOnly() const { return !(m_Stat.st_mode & S_IWUSR); } + bool iasReparsePoint() const { return S_ISLNK(m_Stat.st_mode); } + bool isSparse() const { return false; } + bool isSystem() const { return false; } + bool isTemporary() const { return false; } + +private: + bool m_Valid; + std::filesystem::path m_Path; + struct stat m_Stat; +}; + +#endif // _WIN32 + +class FileBase +{ +public: // Constructors, destructor, assignment. +#ifdef _WIN32 + FileBase() noexcept : m_Handle{INVALID_HANDLE_VALUE} {} + + FileBase(FileBase&& other) noexcept : m_Handle{other.m_Handle} + { + other.m_Handle = INVALID_HANDLE_VALUE; + } +#else + FileBase() noexcept : m_Fd{-1} {} + + FileBase(FileBase&& other) noexcept : m_Fd{other.m_Fd} + { + other.m_Fd = -1; + } +#endif + + ~FileBase() noexcept { Close(); } + + FileBase(FileBase const&) = delete; + FileBase& operator=(FileBase const&) = delete; + FileBase& operator=(FileBase&&) = delete; + +public: // Operations + bool Close() noexcept; + + bool GetPosition(UInt64& position) noexcept; + bool GetLength(UInt64& length) const noexcept; + +#ifdef _WIN32 + bool Seek(Int64 distanceToMove, DWORD moveMethod, UInt64& newPosition) noexcept; +#else + bool Seek(Int64 distanceToMove, int whence, UInt64& newPosition) noexcept; +#endif + bool Seek(UInt64 position, UInt64& newPosition) noexcept; + bool SeekToBegin() noexcept; + bool SeekToEnd(UInt64& newPosition) noexcept; + + // Note: Only the static version (unlike in 7z) because I want FileInfo to hold the + // path to the file, and the non-static version is never used (except by the static + // version). + static bool GetFileInformation(std::filesystem::path const& path, + FileInfo* info) noexcept; + +protected: +#ifdef _WIN32 + bool Create(std::filesystem::path const& path, DWORD desiredAccess, DWORD shareMode, + DWORD creationDisposition, DWORD flagsAndAttributes) noexcept; +#else + bool Create(std::filesystem::path const& path, int flags, int mode = 0644) noexcept; +#endif + +protected: + static constexpr UInt32 kChunkSizeMax = (1 << 22); + +#ifdef _WIN32 + HANDLE m_Handle; +#else + int m_Fd; +#endif +}; + +class FileIn : public FileBase +{ +public: + using FileBase::FileBase; + +public: // Operations +#ifdef _WIN32 + bool Open(std::filesystem::path const& filepath, DWORD shareMode, + DWORD creationDisposition, DWORD flagsAndAttributes) noexcept; + bool OpenShared(std::filesystem::path const& filepath, bool shareForWrite) noexcept; +#endif + bool Open(std::filesystem::path const& filepath) noexcept; + + bool Read(void* data, UInt32 size, UInt32& processedSize) noexcept; + +protected: + bool Read1(void* data, UInt32 size, UInt32& processedSize) noexcept; + bool ReadPart(void* data, UInt32 size, UInt32& processedSize) noexcept; +}; + +class FileOut : public FileBase +{ +public: + using FileBase::FileBase; + +public: // Operations: +#ifdef _WIN32 + bool Open(std::filesystem::path const& fileName, DWORD shareMode, + DWORD creationDisposition, DWORD flagsAndAttributes) noexcept; +#endif + bool Open(std::filesystem::path const& fileName) noexcept; + + bool SetTime(const FILETIME* cTime, const FILETIME* aTime, + const FILETIME* mTime) noexcept; + bool SetMTime(const FILETIME* mTime) noexcept; + bool Write(const void* data, UInt32 size, UInt32& processedSize) noexcept; + + bool SetLength(UInt64 length) noexcept; + bool SetEndOfFile() noexcept; + +protected: // Protected Operations: + bool WritePart(const void* data, UInt32 size, UInt32& processedSize) noexcept; +}; + +/** + * @brief Convert the given wide-string to a path object. + * + * On Windows: adds the long-path prefix if not present. + * On Linux: simply converts wstring to path (no long-path prefix needed). + * + * @param path The string containing the path. + * + * @return the created path. + */ +inline std::filesystem::path make_path(std::wstring const& pathstr) +{ + namespace fs = std::filesystem; + +#ifdef _WIN32 + constexpr const wchar_t* lprefix = L"\\\\?\\"; + constexpr const wchar_t* unc_prefix = L"\\\\"; + constexpr const wchar_t* unc_lprefix = L"\\\\?\\UNC\\"; + + // If path is already a long path, just return it: + if (pathstr.starts_with(lprefix)) { + return fs::path{pathstr}.make_preferred(); + } + + fs::path path{pathstr}; + + // Convert to an absolute path: + if (!path.is_absolute()) { + path = fs::absolute(path); + } + + // backslashes + path = path.make_preferred(); + + // Get rid of duplicate separators and relative moves + path = path.lexically_normal(); + + const std::wstring pathstr_fixed = path.native(); + + // If this is a UNC, the prefix is different + if (pathstr_fixed.starts_with(unc_prefix)) { + return fs::path{unc_lprefix + pathstr_fixed.substr(2)}; + } + + // Add the long-path prefix + return fs::path{lprefix + pathstr_fixed}; +#else + // On Linux, simply convert wstring to path (no long-path prefix needed) + fs::path path{pathstr}; + + if (!path.is_absolute()) { + path = fs::absolute(path); + } + + path = path.lexically_normal(); + return path; +#endif +} + +} // namespace IO + +#endif diff --git a/libs/archive/src/formatter.h b/libs/archive/src/formatter.h new file mode 100644 index 0000000..0776bee --- /dev/null +++ b/libs/archive/src/formatter.h @@ -0,0 +1,121 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2020 MO2 Team. 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 ARCHIVE_FORMAT_H +#define ARCHIVE_FORMAT_H + +// This header specialize formatter() for some useful types. It should not be +// exposed outside of the library. It also contains some useful methods for +// string manipulation. + +#include + +#include +#include +#include +#include + +template <> +struct std::formatter : std::formatter +{ + template + auto format(std::string const& s, FormatContext& ctx) const + { + return std::formatter::format( + std::wstring(s.begin(), s.end()), ctx); + } +}; + +template <> +struct std::formatter : std::formatter +{ + template + auto format(std::exception const& ex, FormatContext& ctx) const + { + return std::formatter::format(ex.what(), ctx); + } +}; + +template <> +struct std::formatter : std::formatter +{ + template + auto format(std::error_code const& ec, FormatContext& ctx) const + { + return std::formatter::format(ec.message(), ctx); + } +}; + +template <> +struct std::formatter + : std::formatter +{ + template + auto format(std::filesystem::path const& path, FormatContext& ctx) const + { + return std::formatter::format(path.wstring(), ctx); + } +}; + +namespace ArchiveStrings +{ + +/** + * @brief Join the element of the given container using the given separator. + * + * @param c The container. Must be satisfy standard container requirements. + * @param sep The separator. + * + * @return a string containing the element joint, or an empty string if c + * is empty. + */ +template +std::wstring join(C const& c, std::wstring const& sep) +{ + auto begin = std::begin(c), end = std::end(c); + + if (begin == end) { + return {}; + } + std::wstring r = *begin++; + for (; begin != end; ++begin) { + r += *begin + sep; + } + + return r; +} + +/** + * @brief Conver the given string to lowercase. + * + * @param s The string to convert. + * + * @return the converted string. + */ +inline std::wstring towlower(std::wstring s) +{ + std::transform(std::begin(s), std::end(s), std::begin(s), [](wchar_t c) { + return static_cast(::towlower(c)); + }); + return s; +} +} // namespace ArchiveStrings + +#endif diff --git a/libs/archive/src/inputstream.cpp b/libs/archive/src/inputstream.cpp new file mode 100644 index 0000000..b44986a --- /dev/null +++ b/libs/archive/src/inputstream.cpp @@ -0,0 +1,70 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 "inputstream.h" +#include "compat.h" + +static inline HRESULT ConvertBoolToHRESULT(bool result) +{ + if (result) { + return S_OK; + } + DWORD lastError = ::GetLastError(); + if (lastError == 0) { + return E_FAIL; + } + return HRESULT_FROM_WIN32(lastError); +} + +InputStream::InputStream() {} + +InputStream::~InputStream() {} + +bool InputStream::Open(std::filesystem::path const& filename) +{ + return m_File.Open(filename); +} + +STDMETHODIMP InputStream::Read(void* data, UInt32 size, UInt32* processedSize) throw() +{ + UInt32 realProcessedSize; + bool result = m_File.Read(data, size, realProcessedSize); + + if (processedSize != nullptr) { + *processedSize = realProcessedSize; + } + + if (result) { + return S_OK; + } + + return HRESULT_FROM_WIN32(::GetLastError()); +} + +STDMETHODIMP InputStream::Seek(Int64 offset, UInt32 seekOrigin, UInt64* newPosition) throw() +{ + UInt64 realNewPosition = offset; + bool result = m_File.Seek(offset, seekOrigin, realNewPosition); + + if (newPosition) { + *newPosition = realNewPosition; + } + return ConvertBoolToHRESULT(result); +} diff --git a/libs/archive/src/inputstream.h b/libs/archive/src/inputstream.h new file mode 100644 index 0000000..b305137 --- /dev/null +++ b/libs/archive/src/inputstream.h @@ -0,0 +1,54 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 INPUTSTREAM_H +#define INPUTSTREAM_H + +#include <7zip/IStream.h> + +#include + +#include "fileio.h" +#include "unknown_impl.h" + +/** This class implements an input stream for opening archive files + * + * Note that the handling on errors could be better. + */ +class InputStream : public IInStream +{ + + UNKNOWN_1_INTERFACE(IInStream); + +public: + InputStream(); + + virtual ~InputStream(); + + bool Open(std::filesystem::path const& filename); + + STDMETHOD(Read)(void* data, UInt32 size, UInt32* processedSize) throw(); + STDMETHOD(Seek)(Int64 offset, UInt32 seekOrigin, UInt64* newPosition) throw(); + +private: + IO::FileIn m_File; +}; + +#endif // INPUTSTREAM_H diff --git a/libs/archive/src/instrument.h b/libs/archive/src/instrument.h new file mode 100644 index 0000000..82dc3f7 --- /dev/null +++ b/libs/archive/src/instrument.h @@ -0,0 +1,111 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2020 MO2 Team. 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 ARCHIVE_INSTRUMENT_H +#define ARCHIVE_INSTRUMENT_H + +#include +#include + +namespace ArchiveTimers +{ + +/** + * If INSTRUMENT_ARCHIVE is not defined, we define a Timer() class that is empty and + * does nothing, and thus should be optimized out by the compiler. + */ +#ifdef INSTRUMENT_ARCHIVE + +/** + * Small class that can be used to instrument portion of code using a guard. + * + */ +class Timer +{ +public: + using clock_t = std::chrono::system_clock; + + struct TimerGuard + { + + TimerGuard(TimerGuard const&) = delete; + TimerGuard(TimerGuard&&) = delete; + TimerGuard& operator=(TimerGuard const&) = delete; + TimerGuard& operator=(TimerGuard&&) = delete; + + ~TimerGuard() + { + m_Timer.ncalls++; + m_Timer.time += clock_t::now() - m_Start; + } + + private: + TimerGuard(Timer& timer) : m_Timer{timer}, m_Start{clock_t::now()} {} + + Timer& m_Timer; + clock_t::time_point m_Start; + + friend class Timer; + }; + + /** + * + */ + Timer() = default; + + /** + * @brief Instrument a portion of code. + * + * Instrumenting is done by calling `instrument()` and storing the result in a local + * variable. The scope of the local guard variable is the scope of the + * instrumentation. + * + * @return a guard to instrument the code. + */ + TimerGuard instrument() { return {*this}; } + + std::wstring toString(std::wstring const& name) const + { + auto ms = [](auto&& t) { + return std::chrono::duration(t); + }; + return std::format( + L"Instrument '{}': {} calls, total of {}ms, {:.3f}ms per call on average.", + name, ncalls, ms(time).count(), ms(time).count() / ncalls); + } + +private: + std::size_t ncalls{0}; + clock_t::duration time{0}; +}; + +#else + +struct Timer +{ + // This is the only method needed: + Timer& instrument() { return *this; } +}; + +#endif + +} // namespace ArchiveTimers + +#endif diff --git a/libs/archive/src/interfaceguids.cpp b/libs/archive/src/interfaceguids.cpp new file mode 100644 index 0000000..ae6659b --- /dev/null +++ b/libs/archive/src/interfaceguids.cpp @@ -0,0 +1,37 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 +*/ + +// This file instantiates the GUIDs needed for linking with the 7zip code +#ifdef _WIN32 +#include +#else +#define INITGUID +#include +#endif + +// extractcallback, opencallback +#include <7zip/IPassword.h> + +// archive, opencallback +// Note: In a sense, this is unnecessary as IArchive.h includes it +#include <7zip/IStream.h> + +// archive, opencallback +#include <7zip/Archive/IArchive.h> diff --git a/libs/archive/src/library.h b/libs/archive/src/library.h new file mode 100644 index 0000000..9fec56d --- /dev/null +++ b/libs/archive/src/library.h @@ -0,0 +1,164 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2020 MO2 Team. 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 ARCHIVE_LIBRARY_H +#define ARCHIVE_LIBRARY_H + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#include +#include +#endif + +/** + * Very small wrapper around shared libraries (DLL on Windows, .so on Linux). + */ +class ALibrary +{ +public: +#ifdef _WIN32 + ALibrary(const char* path) : m_Module{nullptr}, m_LastError{ERROR_SUCCESS} + { + m_Module = LoadLibraryA(path); + if (m_Module == nullptr) { + updateLastError(); + } + } + + ~ALibrary() + { + if (m_Module) { + FreeLibrary(m_Module); + } + } + + template + T resolve(const char* procName) + { + if (!m_Module) { + return nullptr; + } + auto proc = GetProcAddress(m_Module, procName); + if (!proc) { + updateLastError(); + return nullptr; + } + return reinterpret_cast(proc); + } + + DWORD getLastError() const { return m_LastError; } + bool isOpen() const { return m_Module != nullptr; } + operator bool() const { return isOpen(); } + +private: + void updateLastError() { m_LastError = ::GetLastError(); } + + HMODULE m_Module; + DWORD m_LastError; + +#else // Linux + + ALibrary(const char* path) : m_Handle{nullptr}, m_LastError{0} + { + // Find the directory containing our own executable + std::string exeDir; + char selfPath[4096]; + ssize_t len = readlink("/proc/self/exe", selfPath, sizeof(selfPath) - 1); + if (len > 0) { + selfPath[len] = '\0'; + exeDir = std::string(selfPath); + auto slash = exeDir.rfind('/'); + if (slash != std::string::npos) + exeDir = exeDir.substr(0, slash); + } + + // AppImage: check MO2_DLLS_DIR env var first (writable dir next to AppImage) + std::string envDlls; + const char* envVal = std::getenv("MO2_DLLS_DIR"); + if (envVal && envVal[0] != '\0') { + envDlls = envVal; + } + + // Try bundled 7z.so locations first (env override, exe dir, dlls/ subdir) + std::vector tryPaths; + if (!envDlls.empty()) { + tryPaths.push_back(envDlls + "/7z.so"); + } + tryPaths.push_back(exeDir + "/7z.so"); + tryPaths.push_back(exeDir + "/dlls/7z.so"); + tryPaths.push_back("7z.so"); + tryPaths.push_back("dlls/7z.so"); + tryPaths.push_back("/usr/lib/p7zip/7z.so"); + tryPaths.push_back("/usr/lib64/p7zip/7z.so"); + tryPaths.push_back("/usr/libexec/p7zip/7z.so"); + + for (const auto& tryPath : tryPaths) { + m_Handle = dlopen(tryPath.c_str(), RTLD_LAZY); + if (m_Handle) break; + } + + if (!m_Handle) { + // Last resort: try the original path as-is + m_Handle = dlopen(path, RTLD_LAZY); + } + + if (!m_Handle) { + m_LastError = 1; + } + } + + ~ALibrary() + { + if (m_Handle) { + dlclose(m_Handle); + } + } + + template + T resolve(const char* procName) + { + if (!m_Handle) { + return nullptr; + } + void* sym = dlsym(m_Handle, procName); + if (!sym) { + m_LastError = 1; + return nullptr; + } + return reinterpret_cast(sym); + } + + DWORD getLastError() const { return m_LastError; } + bool isOpen() const { return m_Handle != nullptr; } + operator bool() const { return isOpen(); } + +private: + void* m_Handle; + DWORD m_LastError; + +#endif +}; + +#endif diff --git a/libs/archive/src/multioutputstream.cpp b/libs/archive/src/multioutputstream.cpp new file mode 100644 index 0000000..a2eca06 --- /dev/null +++ b/libs/archive/src/multioutputstream.cpp @@ -0,0 +1,142 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 "multioutputstream.h" +#include "compat.h" + +#ifdef _WIN32 +#include +#include +#endif + +static inline HRESULT ConvertBoolToHRESULT(bool result) +{ + if (result) { + return S_OK; + } + DWORD lastError = ::GetLastError(); + if (lastError == 0) { + return E_FAIL; + } + return HRESULT_FROM_WIN32(lastError); +} + +////////////////////////// +// MultiOutputStream + +MultiOutputStream::MultiOutputStream(WriteCallback callback) : m_WriteCallback(callback) +{} + +MultiOutputStream::~MultiOutputStream() {} + +HRESULT MultiOutputStream::Close() +{ + for (auto& file : m_Files) { + file.Close(); + } + return S_OK; +} + +bool MultiOutputStream::Open(std::vector const& filepaths) +{ + m_ProcessedSize = 0; + bool ok = true; + m_Files.clear(); + for (auto& path : filepaths) { + m_Files.emplace_back(); + if (!m_Files.back().Open(path)) { + ok = false; + } + } + return ok; +} + +STDMETHODIMP MultiOutputStream::Write(const void* data, UInt32 size, + UInt32* processedSize) throw() +{ + bool update_processed(true); + for (auto& file : m_Files) { + UInt32 realProcessedSize; + if (!file.Write(data, size, realProcessedSize)) { + return ConvertBoolToHRESULT(false); + } + if (update_processed) { + m_ProcessedSize += realProcessedSize; + if (m_WriteCallback) { + m_WriteCallback(realProcessedSize, m_ProcessedSize); + } + update_processed = false; + } + if (processedSize != nullptr) { + *processedSize = realProcessedSize; + } + } + return S_OK; +} + +STDMETHODIMP MultiOutputStream::Seek(Int64 offset, UInt32 seekOrigin, + UInt64* newPosition) throw() +{ + if (seekOrigin >= 3) + return STG_E_INVALIDFUNCTION; + + bool result = true; + for (auto& file : m_Files) { + UInt64 realNewPosition; + result = file.Seek(offset, seekOrigin, realNewPosition); + if (newPosition) + *newPosition = realNewPosition; + } + return ConvertBoolToHRESULT(result); +} + +STDMETHODIMP MultiOutputStream::SetSize(UInt64 newSize) throw() +{ + bool result = true; + for (auto& file : m_Files) { + UInt64 currentPos; +#ifdef _WIN32 + if (!file.Seek(0, FILE_CURRENT, currentPos)) +#else + if (!file.Seek(0, SEEK_CUR, currentPos)) +#endif + return E_FAIL; + bool cresult = file.SetLength(newSize); + UInt64 currentPos2; + result = result && cresult && file.Seek(currentPos, currentPos2); + } + return result ? S_OK : E_FAIL; +} + +HRESULT MultiOutputStream::GetSize(UInt64* size) +{ + if (m_Files.empty()) { + return ConvertBoolToHRESULT(false); + } + return ConvertBoolToHRESULT(m_Files[0].GetLength(*size)); +} + +bool MultiOutputStream::SetMTime(FILETIME const* mTime) +{ + for (auto& file : m_Files) { + file.SetMTime(mTime); + } + return true; +} diff --git a/libs/archive/src/multioutputstream.h b/libs/archive/src/multioutputstream.h new file mode 100644 index 0000000..624ab5b --- /dev/null +++ b/libs/archive/src/multioutputstream.h @@ -0,0 +1,106 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 MULTIOUTPUTSTREAM_H +#define MULTIOUTPUTSTREAM_H + +#include +#include +#include +#include + +#include <7zip/IStream.h> + +#include "fileio.h" +#include "unknown_impl.h" + +/** This class allows you to open and output to multiple file handles at a time. + * It implements the ISequentalOutputStream interface and has some extra functions + * which are used by the CArchiveExtractCallback class to basically open and + * set the timestamp on all the files. + * + * Note that the handling on errors could be better. + */ +class MultiOutputStream : public IOutStream +{ + + UNKNOWN_1_INTERFACE(IOutStream); + +public: + // Callback for write. Args are: 1) number of bytes that have been + // written for this call, 2) number of bytes that have been written + // in total. + using WriteCallback = std::function; + + MultiOutputStream(WriteCallback callback = {}); + + virtual ~MultiOutputStream(); + + /** Opens the supplied files. + * + * @returns true if all went OK, false if any file failed to open + */ + bool Open(std::vector const& fileNames); + + /** Closes all the files opened by the last open + * + * Note if there are any errors, the code will merely report the last one. + */ + HRESULT Close(); + + /** Sets the modification time on the open files + * + * @returns true if all files had the time set succesfully, false otherwise + * note that this will give up as soon as it gets an error + */ + bool SetMTime(FILETIME const* mTime); + + // ISequentialOutStream interface + + /** Write data to all the streams + * + * The processedSize returned will be the same as size, unless + * there was an error, in which case it might or might not be different. + * @warn If an error happens, the code will not attempt any further writing, + * so some files might not get written to at all + */ + STDMETHOD(Write)(const void* data, UInt32 size, UInt32* processedSize) throw() override; + + STDMETHOD(Seek)(Int64 offset, UInt32 seekOrigin, UInt64* newPosition) throw() override; + STDMETHOD(SetSize)(UInt64 newSize) throw() override; + HRESULT GetSize(UInt64* size); + +private: + WriteCallback m_WriteCallback; + + /** This is the amount of data written to *any one* file. + * + * If there are errors writing to one of the files, this might or might + * not match what was actually written to another of the files + */ + UInt64 m_ProcessedSize; + + /** All the files opened for this 'stream' + * + */ + std::vector m_Files; +}; + +#endif // MULTIOUTPUTSTREAM_H diff --git a/libs/archive/src/opencallback.cpp b/libs/archive/src/opencallback.cpp new file mode 100644 index 0000000..9dd0e24 --- /dev/null +++ b/libs/archive/src/opencallback.cpp @@ -0,0 +1,168 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 "opencallback.h" +#include "compat.h" + +#include "inputstream.h" +#include "propertyvariant.h" + +#include +#include +#include +#include +#include + +#include "fileio.h" + +#define UNUSED(x) + +CArchiveOpenCallback::CArchiveOpenCallback(Archive::PasswordCallback passwordCallback, + Archive::LogCallback logCallback, + std::filesystem::path const& filepath) + : m_PasswordCallback(passwordCallback), m_LogCallback(logCallback), + m_Path(filepath), m_SubArchiveMode(false) +{ + if (!exists(filepath)) { + throw std::runtime_error("invalid archive path"); + } + + if (!IO::FileBase::GetFileInformation(filepath, &m_FileInfo)) { + throw std::runtime_error("failed to retrieve file information"); + } +} + +/* -------------------- IArchiveOpenCallback -------------------- */ +STDMETHODIMP CArchiveOpenCallback::SetTotal(const UInt64* UNUSED(files), + const UInt64* UNUSED(bytes)) throw() +{ + return S_OK; +} + +STDMETHODIMP CArchiveOpenCallback::SetCompleted(const UInt64* UNUSED(files), + const UInt64* UNUSED(bytes)) throw() +{ + return S_OK; +} + +/* -------------------- ICryptoGetTextPassword -------------------- */ +/* This apparently implements ICryptoGetTextPassword but even with a passworded + * archive it doesn't seem to be called. There is also another API which isn't + * implemented. + */ +STDMETHODIMP CArchiveOpenCallback::CryptoGetTextPassword(BSTR* passwordOut) throw() +{ + if (!m_PasswordCallback) { + return E_ABORT; + } + m_Password = m_PasswordCallback(); + *passwordOut = ::SysAllocString(m_Password.c_str()); + return *passwordOut != 0 ? S_OK : E_OUTOFMEMORY; +} + +/* -------------------- IArchiveOpenSetSubArchiveName -------------------- */ +/* I don't know what this does or how you call it. */ +STDMETHODIMP CArchiveOpenCallback::SetSubArchiveName(const wchar_t* name) throw() +{ + m_SubArchiveMode = true; + m_SubArchiveName = name; + return S_OK; +} + +/* -------------------- IArchiveOpenVolumeCallback -------------------- */ + +STDMETHODIMP CArchiveOpenCallback::GetProperty(PROPID propID, + PROPVARIANT* value) throw() +{ + // A scan of the source code indicates that the only things that ever call the + // IArchiveOpenVolumeCallback interface ask for the file name and size. + PropertyVariant& prop = *static_cast(value); + + switch (propID) { + case kpidName: { + if (m_SubArchiveMode) { + prop = m_SubArchiveName; + } else { + // Note: Need to call .native(), otherwize we get a link error because we try to + // assign a fs::path to the variant. + prop = m_Path.filename().wstring(); + } + } break; + + case kpidIsDir: + prop = m_FileInfo.isDir(); + break; + case kpidSize: + prop = m_FileInfo.fileSize(); + break; + case kpidAttrib: + prop = m_FileInfo.fileAttributes(); + break; + case kpidCTime: + prop = m_FileInfo.creationTime(); + break; + case kpidATime: + prop = m_FileInfo.lastAccessTime(); + break; + case kpidMTime: + prop = m_FileInfo.lastWriteTime(); + break; + + default: + m_LogCallback(Archive::LogLevel::Warning, + std::format(L"Unexpected property {}.", propID)); + } + return S_OK; +} + +STDMETHODIMP CArchiveOpenCallback::GetStream(const wchar_t* name, + IInStream** inStream) throw() +{ + *inStream = nullptr; + + // this function will be called repeatedly for split archives, `name` will + // have increasing numbers in the extension and S_FALSE must be returned + // when a filename doesn't exist so the search stops + + if (!name) { + return S_FALSE; + } + + // `name` is just the filename, so build a path from the directory that + // contained the last file + const auto path = m_Path.parent_path() / name; + + if (!exists(m_FileInfo.path()) || m_FileInfo.isDir()) { + return S_FALSE; + } + + if (!IO::FileBase::GetFileInformation(path, &m_FileInfo)) { + return S_FALSE; + } + + CComPtr inFile(new InputStream); + + if (!inFile->Open(m_FileInfo.path())) { + return ::GetLastError(); + } + + *inStream = inFile.Detach(); + return S_OK; +} diff --git a/libs/archive/src/opencallback.h b/libs/archive/src/opencallback.h new file mode 100644 index 0000000..44367be --- /dev/null +++ b/libs/archive/src/opencallback.h @@ -0,0 +1,75 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 OPENCALLBACK_H +#define OPENCALLBACK_H + +#include +#include + +#include <7zip/Archive/IArchive.h> +#include <7zip/IPassword.h> + +#include "archive.h" +#include "fileio.h" +#include "unknown_impl.h" + +class CArchiveOpenCallback : public IArchiveOpenCallback, + public IArchiveOpenVolumeCallback, + public ICryptoGetTextPassword, + public IArchiveOpenSetSubArchiveName +{ + + UNKNOWN_4_INTERFACE(IArchiveOpenCallback, IArchiveOpenVolumeCallback, + ICryptoGetTextPassword, IArchiveOpenSetSubArchiveName); + +public: + CArchiveOpenCallback(Archive::PasswordCallback passwordCallback, + Archive::LogCallback logCallback, + std::filesystem::path const& filepath); + + virtual ~CArchiveOpenCallback() {} + + const std::wstring& GetPassword() const { return m_Password; } + + Z7_IFACE_COM7_IMP(IArchiveOpenCallback) + Z7_IFACE_COM7_IMP(IArchiveOpenVolumeCallback) + + // ICryptoGetTextPassword interface + STDMETHOD(CryptoGetTextPassword)(BSTR* password) throw(); + // Not implemented STDMETHOD(CryptoGetTextPassword2)(Int32 *passwordIsDefined, BSTR + // *password); + + // IArchiveOpenSetSubArchiveName interface + STDMETHOD(SetSubArchiveName)(const wchar_t* name) throw(); + +private: + Archive::PasswordCallback m_PasswordCallback; + Archive::LogCallback m_LogCallback; + std::wstring m_Password; + + std::filesystem::path m_Path; + IO::FileInfo m_FileInfo; + + bool m_SubArchiveMode; + std::wstring m_SubArchiveName; +}; + +#endif // OPENCALLBACK_H diff --git a/libs/archive/src/propertyvariant.cpp b/libs/archive/src/propertyvariant.cpp new file mode 100644 index 0000000..fe882b2 --- /dev/null +++ b/libs/archive/src/propertyvariant.cpp @@ -0,0 +1,214 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 "propertyvariant.h" +#include "compat.h" + +#ifdef _WIN32 +#include +#endif + +#include +#include +#include + +PropertyVariant::PropertyVariant() +{ + PropVariantInit(this); +} + +PropertyVariant::~PropertyVariant() +{ + clear(); +} + +void PropertyVariant::clear() +{ + PropVariantClear(this); +} + +// Arguably the behviours for empty here are wrong. +template <> +PropertyVariant::operator bool() const +{ + switch (vt) { + case VT_EMPTY: + return false; + + case VT_BOOL: + return boolVal != VARIANT_FALSE; + + default: + throw std::runtime_error("Property is not a bool"); + } +} + +template <> +PropertyVariant::operator uint64_t() const +{ + switch (vt) { + case VT_EMPTY: + return 0; + + case VT_UI1: + return bVal; + + case VT_UI2: + return uiVal; + + case VT_UI4: + return ulVal; + + case VT_UI8: + return static_cast(uhVal.QuadPart); + + default: + throw std::runtime_error("Property is not an unsigned integer"); + } +} + +template <> +PropertyVariant::operator uint32_t() const +{ + switch (vt) { + case VT_EMPTY: + return 0; + + case VT_UI1: + return bVal; + + case VT_UI2: + return uiVal; + + case VT_UI4: + return ulVal; + + default: + throw std::runtime_error("Property is not an unsigned integer"); + } +} + +template <> +PropertyVariant::operator std::wstring() const +{ + switch (vt) { + case VT_EMPTY: + return L""; + + case VT_BSTR: + return std::wstring(bstrVal, ::SysStringLen(bstrVal)); + + default: + throw std::runtime_error("Property is not a string"); + } +} + +// This is what he does, though it looks rather a strange use of the property +template <> +PropertyVariant::operator std::string() const +{ + switch (vt) { + case VT_EMPTY: + return ""; + + case VT_BSTR: + // If he can do a memcpy, I can do a reinterpret case + return std::string(reinterpret_cast(bstrVal), + ::SysStringByteLen(bstrVal)); + + default: + throw std::runtime_error("Property is not a string"); + } +} + +// This is what he does, though it looks rather a strange use of the property +template <> +PropertyVariant::operator GUID() const +{ + switch (vt) { + case VT_BSTR: + // He did a cast too! + return *reinterpret_cast(bstrVal); + + default: + throw std::runtime_error("Property is not a GUID (string)"); + } +} + +template <> +PropertyVariant::operator FILETIME() const +{ + switch (vt) { + case VT_FILETIME: + return filetime; + + default: + throw std::runtime_error("Property is not a file time"); + } +} + +// Assignments +template <> +PropertyVariant& PropertyVariant::operator=(std::wstring const& str) +{ + clear(); + vt = VT_BSTR; + bstrVal = ::SysAllocString(str.c_str()); + if (bstrVal == NULL) { + throw std::bad_alloc(); + } + return *this; +} + +template <> +PropertyVariant& PropertyVariant::operator=(bool const& n) +{ + clear(); + vt = VT_BOOL; + boolVal = n ? VARIANT_TRUE : VARIANT_FALSE; + return *this; +} + +template <> +PropertyVariant& PropertyVariant::operator=(FILETIME const& n) +{ + clear(); + vt = VT_FILETIME; + filetime = n; + return *this; +} + +template <> +PropertyVariant& PropertyVariant::operator=(uint32_t const& n) +{ + clear(); + vt = VT_UI4; + ulVal = n; + return *this; +} + +template <> +PropertyVariant& PropertyVariant::operator=(uint64_t const& n) +{ + clear(); + vt = VT_UI8; + uhVal.QuadPart = n; + return *this; +} diff --git a/libs/archive/src/propertyvariant.h b/libs/archive/src/propertyvariant.h new file mode 100644 index 0000000..0748d94 --- /dev/null +++ b/libs/archive/src/propertyvariant.h @@ -0,0 +1,56 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 PROPERTYVARIANT_H +#define PROPERTYVARIANT_H + +#ifdef _WIN32 +#include +#else +#include +#endif + +/** This class implements a wrapper round the PROPVARIANT structure which + * makes it a little friendler to use and a little more C++ish + * + * Sadly the inheritance needs to be public. Once you have a pointer to the + * base class you can do pretty much anything to it anyway. + */ +class PropertyVariant : public tagPROPVARIANT +{ +public: + PropertyVariant(); + ~PropertyVariant(); + + // clears the property should you wish to overwrite it with another one. + void clear(); + + bool is_empty() { return vt == VT_EMPTY; } + + // It's too much like hard work listing all the valid ones. If it doesn't + // link, then you need to implement it... + template + explicit operator T() const; + + template + PropertyVariant& operator=(T const&); +}; + +#endif // PROPVARIANT_H diff --git a/libs/archive/src/unknown_impl.h b/libs/archive/src/unknown_impl.h new file mode 100644 index 0000000..78e9f4a --- /dev/null +++ b/libs/archive/src/unknown_impl.h @@ -0,0 +1,191 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. 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 UNKNOWN_IMPL_H +#define UNKNOWN_IMPL_H + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#endif + +/* This implements a common way of creating classes which implement one or more + * COM interfaces. + * + * Your implementaton should be something like this + * + * class MyImplementation : + * public interface1, + * public interface2 //etc to taste + * { + * UNKNOWN_1_INTERFACE(interface1); + * or UNKNOWN_2_INTERFACE(interface1, interface2); etc + * + * It would probably be possible to do something very magic with macros + * that allowed you to do + * class My_Implmentation : UNKNOWN_IMPL_n(interface1, ...) + * + * that did all the above for you, but I can't see how to maintain the braces + * in order to make the class declaration look nice. + * + * You might ask why I have e-implemented this from the 7-zip stuff (sort of). + * + * Well, + * + * Firstly, the 7zip macros are slightly buggy (they don't null the outpointer + * if the interface isn't found), and they aren't thread safe. + * + * Secondly, although 7-zip has a requirement to run on platforms other than + * windows, we don't. So I changed this to use the Com functionality supplied + * by windows, which (potentially) has better debugging facilities + * + * Thirdly, this removes a number of dependencies on 7-zip source which aren't + * really part of the API + */ + +#ifdef _WIN32 + +/* Do not use these macros, use the ones at the bottom */ +#define UNKNOWN__INTERFACE_BEGIN \ +public: \ + STDMETHOD_(ULONG, AddRef)() \ + { \ + return ::InterlockedIncrement(&m_RefCount__); \ + } \ + \ + STDMETHOD_(ULONG, Release)() \ + { \ + ULONG res = ::InterlockedDecrement(&m_RefCount__); \ + if (res == 0) { \ + delete this; \ + } \ + return res; \ + } \ + \ + STDMETHOD(QueryInterface)(REFGUID iid, void** outObject) \ + { \ + if (iid == IID_IUnknown) { + +#define UNKNOWN__INTERFACE_UNKNOWN(interface) \ + *outObject = static_cast(static_cast(this)); \ + } + +#define UNKNOWN__INTERFACE_NAME(interface) \ + else if (iid == IID_##interface) \ + { \ + *outObject = static_cast(this); \ + } + +#define UNKNOWN__INTERFACE_END \ + else \ + { \ + *outObject = nullptr; \ + return E_NOINTERFACE; \ + } \ + AddRef(); \ + return S_OK; \ + } \ + \ +private: \ + ULONG m_RefCount__ = 0 + +#else // Linux + +/* Do not use these macros, use the ones at the bottom */ +#define UNKNOWN__INTERFACE_BEGIN \ +public: \ + STDMETHOD_(ULONG, AddRef)() \ + { \ + return ++m_RefCount__; \ + } \ + \ + STDMETHOD_(ULONG, Release)() \ + { \ + ULONG res = --m_RefCount__; \ + if (res == 0) { \ + delete this; \ + } \ + return res; \ + } \ + \ + STDMETHOD(QueryInterface)(REFGUID iid, void** outObject) \ + { \ + if (iid == IID_IUnknown) { + +#define UNKNOWN__INTERFACE_UNKNOWN(interface) \ + *outObject = static_cast(static_cast(this)); \ + } + +#define UNKNOWN__INTERFACE_NAME(interface) \ + else if (iid == IID_##interface) \ + { \ + *outObject = static_cast(this); \ + } + +#define UNKNOWN__INTERFACE_END \ + else \ + { \ + *outObject = nullptr; \ + return E_NOINTERFACE; \ + } \ + AddRef(); \ + return S_OK; \ + } \ + \ +private: \ + std::atomic m_RefCount__{0} + +#endif // _WIN32 + +/* These are the macros you should be using */ +#define UNKNOWN_1_INTERFACE(interface) \ + UNKNOWN__INTERFACE_BEGIN \ + UNKNOWN__INTERFACE_UNKNOWN(interface) \ + UNKNOWN__INTERFACE_NAME(interface) \ + UNKNOWN__INTERFACE_END + +#define UNKNOWN_2_INTERFACE(interface1, interface2) \ + UNKNOWN__INTERFACE_BEGIN \ + UNKNOWN__INTERFACE_UNKNOWN(interface1) \ + UNKNOWN__INTERFACE_NAME(interface1) \ + UNKNOWN__INTERFACE_NAME(interface2) \ + UNKNOWN__INTERFACE_END + +#define UNKNOWN_3_INTERFACE(interface1, interface2, interface3) \ + UNKNOWN__INTERFACE_BEGIN \ + UNKNOWN__INTERFACE_UNKNOWN(interface1) \ + UNKNOWN__INTERFACE_NAME(interface1) \ + UNKNOWN__INTERFACE_NAME(interface2) \ + UNKNOWN__INTERFACE_NAME(interface3) \ + UNKNOWN__INTERFACE_END + +#define UNKNOWN_4_INTERFACE(interface1, interface2, interface3, interface4) \ + UNKNOWN__INTERFACE_BEGIN \ + UNKNOWN__INTERFACE_UNKNOWN(interface1) \ + UNKNOWN__INTERFACE_NAME(interface1) \ + UNKNOWN__INTERFACE_NAME(interface2) \ + UNKNOWN__INTERFACE_NAME(interface3) \ + UNKNOWN__INTERFACE_NAME(interface4) \ + UNKNOWN__INTERFACE_END + +#endif // UNKNOWN_IMPL_H diff --git a/libs/archive/src/version.rc b/libs/archive/src/version.rc new file mode 100644 index 0000000..0fb4cf0 --- /dev/null +++ b/libs/archive/src/version.rc @@ -0,0 +1,28 @@ +#include "Winver.h" + +#define VER_FILEVERSION 2,0,0 +#define VER_FILEVERSION_STR 2,0,0 + +VS_VERSION_INFO VERSIONINFO +FILEVERSION VER_FILEVERSION +PRODUCTVERSION VER_FILEVERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS (0) +FILEOS VOS__WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "Mod Organizer 2 Team" + VALUE "FileVersion", VER_FILEVERSION_STR + END +END + +BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1200 +END +END diff --git a/libs/archive/thirdparty/C/7zTypes.h b/libs/archive/thirdparty/C/7zTypes.h new file mode 100644 index 0000000..5b77420 --- /dev/null +++ b/libs/archive/thirdparty/C/7zTypes.h @@ -0,0 +1,597 @@ +/* 7zTypes.h -- Basic types +2024-01-24 : Igor Pavlov : Public domain */ + +#ifndef ZIP7_7Z_TYPES_H +#define ZIP7_7Z_TYPES_H + +#ifdef _WIN32 +/* #include */ +#else +#include +#endif + +#include + +#ifndef EXTERN_C_BEGIN +#ifdef __cplusplus +#define EXTERN_C_BEGIN extern "C" { +#define EXTERN_C_END } +#else +#define EXTERN_C_BEGIN +#define EXTERN_C_END +#endif +#endif + +EXTERN_C_BEGIN + +#define SZ_OK 0 + +#define SZ_ERROR_DATA 1 +#define SZ_ERROR_MEM 2 +#define SZ_ERROR_CRC 3 +#define SZ_ERROR_UNSUPPORTED 4 +#define SZ_ERROR_PARAM 5 +#define SZ_ERROR_INPUT_EOF 6 +#define SZ_ERROR_OUTPUT_EOF 7 +#define SZ_ERROR_READ 8 +#define SZ_ERROR_WRITE 9 +#define SZ_ERROR_PROGRESS 10 +#define SZ_ERROR_FAIL 11 +#define SZ_ERROR_THREAD 12 + +#define SZ_ERROR_ARCHIVE 16 +#define SZ_ERROR_NO_ARCHIVE 17 + +typedef int SRes; + + +#ifdef _MSC_VER + #if _MSC_VER > 1200 + #define MY_ALIGN(n) __declspec(align(n)) + #else + #define MY_ALIGN(n) + #endif +#else + /* + // C11/C++11: + #include + #define MY_ALIGN(n) alignas(n) + */ + #define MY_ALIGN(n) __attribute__ ((aligned(n))) +#endif + + +#ifdef _WIN32 + +/* typedef DWORD WRes; */ +typedef unsigned WRes; +#define MY_SRes_HRESULT_FROM_WRes(x) HRESULT_FROM_WIN32(x) + +// #define MY_HRES_ERROR_INTERNAL_ERROR MY_SRes_HRESULT_FROM_WRes(ERROR_INTERNAL_ERROR) + +#else // _WIN32 + +// #define ENV_HAVE_LSTAT +typedef int WRes; + +// (FACILITY_ERRNO = 0x800) is 7zip's FACILITY constant to represent (errno) errors in HRESULT +#define MY_FACILITY_ERRNO 0x800 +#define MY_FACILITY_WIN32 7 +#define MY_FACILITY_WRes MY_FACILITY_ERRNO + +#define MY_HRESULT_FROM_errno_CONST_ERROR(x) ((HRESULT)( \ + ( (HRESULT)(x) & 0x0000FFFF) \ + | (MY_FACILITY_WRes << 16) \ + | (HRESULT)0x80000000 )) + +#define MY_SRes_HRESULT_FROM_WRes(x) \ + ((HRESULT)(x) <= 0 ? ((HRESULT)(x)) : MY_HRESULT_FROM_errno_CONST_ERROR(x)) + +// we call macro HRESULT_FROM_WIN32 for system errors (WRes) that are (errno) +#define HRESULT_FROM_WIN32(x) MY_SRes_HRESULT_FROM_WRes(x) + +/* +#define ERROR_FILE_NOT_FOUND 2L +#define ERROR_ACCESS_DENIED 5L +#define ERROR_NO_MORE_FILES 18L +#define ERROR_LOCK_VIOLATION 33L +#define ERROR_FILE_EXISTS 80L +#define ERROR_DISK_FULL 112L +#define ERROR_NEGATIVE_SEEK 131L +#define ERROR_ALREADY_EXISTS 183L +#define ERROR_DIRECTORY 267L +#define ERROR_TOO_MANY_POSTS 298L + +#define ERROR_INTERNAL_ERROR 1359L +#define ERROR_INVALID_REPARSE_DATA 4392L +#define ERROR_REPARSE_TAG_INVALID 4393L +#define ERROR_REPARSE_TAG_MISMATCH 4394L +*/ + +// we use errno equivalents for some WIN32 errors: + +#define ERROR_INVALID_PARAMETER EINVAL +#define ERROR_INVALID_FUNCTION EINVAL +#define ERROR_ALREADY_EXISTS EEXIST +#define ERROR_FILE_EXISTS EEXIST +#define ERROR_PATH_NOT_FOUND ENOENT +#define ERROR_FILE_NOT_FOUND ENOENT +#define ERROR_DISK_FULL ENOSPC +// #define ERROR_INVALID_HANDLE EBADF + +// we use FACILITY_WIN32 for errors that has no errno equivalent +// Too many posts were made to a semaphore. +#define ERROR_TOO_MANY_POSTS ((HRESULT)0x8007012AL) +#define ERROR_INVALID_REPARSE_DATA ((HRESULT)0x80071128L) +#define ERROR_REPARSE_TAG_INVALID ((HRESULT)0x80071129L) + +// if (MY_FACILITY_WRes != FACILITY_WIN32), +// we use FACILITY_WIN32 for COM errors: +#define E_OUTOFMEMORY ((HRESULT)0x8007000EL) +#define E_INVALIDARG ((HRESULT)0x80070057L) +#define MY_E_ERROR_NEGATIVE_SEEK ((HRESULT)0x80070083L) + +/* +// we can use FACILITY_ERRNO for some COM errors, that have errno equivalents: +#define E_OUTOFMEMORY MY_HRESULT_FROM_errno_CONST_ERROR(ENOMEM) +#define E_INVALIDARG MY_HRESULT_FROM_errno_CONST_ERROR(EINVAL) +#define MY_E_ERROR_NEGATIVE_SEEK MY_HRESULT_FROM_errno_CONST_ERROR(EINVAL) +*/ + +#define TEXT(quote) quote + +#define FILE_ATTRIBUTE_READONLY 0x0001 +#define FILE_ATTRIBUTE_HIDDEN 0x0002 +#define FILE_ATTRIBUTE_SYSTEM 0x0004 +#define FILE_ATTRIBUTE_DIRECTORY 0x0010 +#define FILE_ATTRIBUTE_ARCHIVE 0x0020 +#define FILE_ATTRIBUTE_DEVICE 0x0040 +#define FILE_ATTRIBUTE_NORMAL 0x0080 +#define FILE_ATTRIBUTE_TEMPORARY 0x0100 +#define FILE_ATTRIBUTE_SPARSE_FILE 0x0200 +#define FILE_ATTRIBUTE_REPARSE_POINT 0x0400 +#define FILE_ATTRIBUTE_COMPRESSED 0x0800 +#define FILE_ATTRIBUTE_OFFLINE 0x1000 +#define FILE_ATTRIBUTE_NOT_CONTENT_INDEXED 0x2000 +#define FILE_ATTRIBUTE_ENCRYPTED 0x4000 + +#define FILE_ATTRIBUTE_UNIX_EXTENSION 0x8000 /* trick for Unix */ + +#endif + + +#ifndef RINOK +#define RINOK(x) { const int _result_ = (x); if (_result_ != 0) return _result_; } +#endif + +#ifndef RINOK_WRes +#define RINOK_WRes(x) { const WRes _result_ = (x); if (_result_ != 0) return _result_; } +#endif + +typedef unsigned char Byte; +typedef short Int16; +typedef unsigned short UInt16; + +#ifdef Z7_DECL_Int32_AS_long +typedef long Int32; +typedef unsigned long UInt32; +#else +typedef int Int32; +typedef unsigned int UInt32; +#endif + + +#ifndef _WIN32 + +typedef int INT; +typedef Int32 INT32; +typedef unsigned int UINT; +typedef UInt32 UINT32; +typedef INT32 LONG; // LONG, ULONG and DWORD must be 32-bit for _WIN32 compatibility +typedef UINT32 ULONG; + +#undef DWORD +typedef UINT32 DWORD; + +#define VOID void + +#define HRESULT LONG + +typedef void *LPVOID; +// typedef void VOID; +// typedef ULONG_PTR DWORD_PTR, *PDWORD_PTR; +// gcc / clang on Unix : sizeof(long==sizeof(void*) in 32 or 64 bits) +typedef long INT_PTR; +typedef unsigned long UINT_PTR; +typedef long LONG_PTR; +typedef unsigned long DWORD_PTR; + +typedef size_t SIZE_T; + +#endif // _WIN32 + + +#define MY_HRES_ERROR_INTERNAL_ERROR ((HRESULT)0x8007054FL) + + +#ifdef Z7_DECL_Int64_AS_long + +typedef long Int64; +typedef unsigned long UInt64; + +#else + +#if (defined(_MSC_VER) || defined(__BORLANDC__)) && !defined(__clang__) +typedef __int64 Int64; +typedef unsigned __int64 UInt64; +#else +#if defined(__clang__) || defined(__GNUC__) +#include +typedef int64_t Int64; +typedef uint64_t UInt64; +#else +typedef long long int Int64; +typedef unsigned long long int UInt64; +// #define UINT64_CONST(n) n ## ULL +#endif +#endif + +#endif + +#define UINT64_CONST(n) n + + +#ifdef Z7_DECL_SizeT_AS_unsigned_int +typedef unsigned int SizeT; +#else +typedef size_t SizeT; +#endif + +/* +#if (defined(_MSC_VER) && _MSC_VER <= 1200) +typedef size_t MY_uintptr_t; +#else +#include +typedef uintptr_t MY_uintptr_t; +#endif +*/ + +typedef int BoolInt; +/* typedef BoolInt Bool; */ +#define True 1 +#define False 0 + + +#ifdef _WIN32 +#define Z7_STDCALL __stdcall +#else +#define Z7_STDCALL +#endif + +#ifdef _MSC_VER + +#if _MSC_VER >= 1300 +#define Z7_NO_INLINE __declspec(noinline) +#else +#define Z7_NO_INLINE +#endif + +#define Z7_FORCE_INLINE __forceinline + +#define Z7_CDECL __cdecl +#define Z7_FASTCALL __fastcall + +#else // _MSC_VER + +#if (defined(__GNUC__) && (__GNUC__ >= 4)) \ + || (defined(__clang__) && (__clang_major__ >= 4)) \ + || defined(__INTEL_COMPILER) \ + || defined(__xlC__) +#define Z7_NO_INLINE __attribute__((noinline)) +#define Z7_FORCE_INLINE __attribute__((always_inline)) inline +#else +#define Z7_NO_INLINE +#define Z7_FORCE_INLINE +#endif + +#define Z7_CDECL + +#if defined(_M_IX86) \ + || defined(__i386__) +// #define Z7_FASTCALL __attribute__((fastcall)) +// #define Z7_FASTCALL __attribute__((cdecl)) +#define Z7_FASTCALL +#elif defined(MY_CPU_AMD64) +// #define Z7_FASTCALL __attribute__((ms_abi)) +#define Z7_FASTCALL +#else +#define Z7_FASTCALL +#endif + +#endif // _MSC_VER + + +/* The following interfaces use first parameter as pointer to structure */ + +// #define Z7_C_IFACE_CONST_QUAL +#define Z7_C_IFACE_CONST_QUAL const + +#define Z7_C_IFACE_DECL(a) \ + struct a ## _; \ + typedef Z7_C_IFACE_CONST_QUAL struct a ## _ * a ## Ptr; \ + typedef struct a ## _ a; \ + struct a ## _ + + +Z7_C_IFACE_DECL (IByteIn) +{ + Byte (*Read)(IByteInPtr p); /* reads one byte, returns 0 in case of EOF or error */ +}; +#define IByteIn_Read(p) (p)->Read(p) + + +Z7_C_IFACE_DECL (IByteOut) +{ + void (*Write)(IByteOutPtr p, Byte b); +}; +#define IByteOut_Write(p, b) (p)->Write(p, b) + + +Z7_C_IFACE_DECL (ISeqInStream) +{ + SRes (*Read)(ISeqInStreamPtr p, void *buf, size_t *size); + /* if (input(*size) != 0 && output(*size) == 0) means end_of_stream. + (output(*size) < input(*size)) is allowed */ +}; +#define ISeqInStream_Read(p, buf, size) (p)->Read(p, buf, size) + +/* try to read as much as avail in stream and limited by (*processedSize) */ +SRes SeqInStream_ReadMax(ISeqInStreamPtr stream, void *buf, size_t *processedSize); +/* it can return SZ_ERROR_INPUT_EOF */ +// SRes SeqInStream_Read(ISeqInStreamPtr stream, void *buf, size_t size); +// SRes SeqInStream_Read2(ISeqInStreamPtr stream, void *buf, size_t size, SRes errorType); +SRes SeqInStream_ReadByte(ISeqInStreamPtr stream, Byte *buf); + + +Z7_C_IFACE_DECL (ISeqOutStream) +{ + size_t (*Write)(ISeqOutStreamPtr p, const void *buf, size_t size); + /* Returns: result - the number of actually written bytes. + (result < size) means error */ +}; +#define ISeqOutStream_Write(p, buf, size) (p)->Write(p, buf, size) + +typedef enum +{ + SZ_SEEK_SET = 0, + SZ_SEEK_CUR = 1, + SZ_SEEK_END = 2 +} ESzSeek; + + +Z7_C_IFACE_DECL (ISeekInStream) +{ + SRes (*Read)(ISeekInStreamPtr p, void *buf, size_t *size); /* same as ISeqInStream::Read */ + SRes (*Seek)(ISeekInStreamPtr p, Int64 *pos, ESzSeek origin); +}; +#define ISeekInStream_Read(p, buf, size) (p)->Read(p, buf, size) +#define ISeekInStream_Seek(p, pos, origin) (p)->Seek(p, pos, origin) + + +Z7_C_IFACE_DECL (ILookInStream) +{ + SRes (*Look)(ILookInStreamPtr p, const void **buf, size_t *size); + /* if (input(*size) != 0 && output(*size) == 0) means end_of_stream. + (output(*size) > input(*size)) is not allowed + (output(*size) < input(*size)) is allowed */ + SRes (*Skip)(ILookInStreamPtr p, size_t offset); + /* offset must be <= output(*size) of Look */ + SRes (*Read)(ILookInStreamPtr p, void *buf, size_t *size); + /* reads directly (without buffer). It's same as ISeqInStream::Read */ + SRes (*Seek)(ILookInStreamPtr p, Int64 *pos, ESzSeek origin); +}; + +#define ILookInStream_Look(p, buf, size) (p)->Look(p, buf, size) +#define ILookInStream_Skip(p, offset) (p)->Skip(p, offset) +#define ILookInStream_Read(p, buf, size) (p)->Read(p, buf, size) +#define ILookInStream_Seek(p, pos, origin) (p)->Seek(p, pos, origin) + + +SRes LookInStream_LookRead(ILookInStreamPtr stream, void *buf, size_t *size); +SRes LookInStream_SeekTo(ILookInStreamPtr stream, UInt64 offset); + +/* reads via ILookInStream::Read */ +SRes LookInStream_Read2(ILookInStreamPtr stream, void *buf, size_t size, SRes errorType); +SRes LookInStream_Read(ILookInStreamPtr stream, void *buf, size_t size); + + +typedef struct +{ + ILookInStream vt; + ISeekInStreamPtr realStream; + + size_t pos; + size_t size; /* it's data size */ + + /* the following variables must be set outside */ + Byte *buf; + size_t bufSize; +} CLookToRead2; + +void LookToRead2_CreateVTable(CLookToRead2 *p, int lookahead); + +#define LookToRead2_INIT(p) { (p)->pos = (p)->size = 0; } + + +typedef struct +{ + ISeqInStream vt; + ILookInStreamPtr realStream; +} CSecToLook; + +void SecToLook_CreateVTable(CSecToLook *p); + + + +typedef struct +{ + ISeqInStream vt; + ILookInStreamPtr realStream; +} CSecToRead; + +void SecToRead_CreateVTable(CSecToRead *p); + + +Z7_C_IFACE_DECL (ICompressProgress) +{ + SRes (*Progress)(ICompressProgressPtr p, UInt64 inSize, UInt64 outSize); + /* Returns: result. (result != SZ_OK) means break. + Value (UInt64)(Int64)-1 for size means unknown value. */ +}; + +#define ICompressProgress_Progress(p, inSize, outSize) (p)->Progress(p, inSize, outSize) + + + +typedef struct ISzAlloc ISzAlloc; +typedef const ISzAlloc * ISzAllocPtr; + +struct ISzAlloc +{ + void *(*Alloc)(ISzAllocPtr p, size_t size); + void (*Free)(ISzAllocPtr p, void *address); /* address can be 0 */ +}; + +#define ISzAlloc_Alloc(p, size) (p)->Alloc(p, size) +#define ISzAlloc_Free(p, a) (p)->Free(p, a) + +/* deprecated */ +#define IAlloc_Alloc(p, size) ISzAlloc_Alloc(p, size) +#define IAlloc_Free(p, a) ISzAlloc_Free(p, a) + + + + + +#ifndef MY_offsetof + #ifdef offsetof + #define MY_offsetof(type, m) offsetof(type, m) + /* + #define MY_offsetof(type, m) FIELD_OFFSET(type, m) + */ + #else + #define MY_offsetof(type, m) ((size_t)&(((type *)0)->m)) + #endif +#endif + + + +#ifndef Z7_container_of + +/* +#define Z7_container_of(ptr, type, m) container_of(ptr, type, m) +#define Z7_container_of(ptr, type, m) CONTAINING_RECORD(ptr, type, m) +#define Z7_container_of(ptr, type, m) ((type *)((char *)(ptr) - offsetof(type, m))) +#define Z7_container_of(ptr, type, m) (&((type *)0)->m == (ptr), ((type *)(((char *)(ptr)) - MY_offsetof(type, m)))) +*/ + +/* + GCC shows warning: "perhaps the 'offsetof' macro was used incorrectly" + GCC 3.4.4 : classes with constructor + GCC 4.8.1 : classes with non-public variable members" +*/ + +#define Z7_container_of(ptr, type, m) \ + ((type *)(void *)((char *)(void *) \ + (1 ? (ptr) : &((type *)NULL)->m) - MY_offsetof(type, m))) + +#define Z7_container_of_CONST(ptr, type, m) \ + ((const type *)(const void *)((const char *)(const void *) \ + (1 ? (ptr) : &((type *)NULL)->m) - MY_offsetof(type, m))) + +/* +#define Z7_container_of_NON_CONST_FROM_CONST(ptr, type, m) \ + ((type *)(void *)(const void *)((const char *)(const void *) \ + (1 ? (ptr) : &((type *)NULL)->m) - MY_offsetof(type, m))) +*/ + +#endif + +#define Z7_CONTAINER_FROM_VTBL_SIMPLE(ptr, type, m) ((type *)(void *)(ptr)) + +// #define Z7_CONTAINER_FROM_VTBL(ptr, type, m) Z7_CONTAINER_FROM_VTBL_SIMPLE(ptr, type, m) +#define Z7_CONTAINER_FROM_VTBL(ptr, type, m) Z7_container_of(ptr, type, m) +// #define Z7_CONTAINER_FROM_VTBL(ptr, type, m) Z7_container_of_NON_CONST_FROM_CONST(ptr, type, m) + +#define Z7_CONTAINER_FROM_VTBL_CONST(ptr, type, m) Z7_container_of_CONST(ptr, type, m) + +#define Z7_CONTAINER_FROM_VTBL_CLS(ptr, type, m) Z7_CONTAINER_FROM_VTBL_SIMPLE(ptr, type, m) +/* +#define Z7_CONTAINER_FROM_VTBL_CLS(ptr, type, m) Z7_CONTAINER_FROM_VTBL(ptr, type, m) +*/ +#if defined (__clang__) || defined(__GNUC__) +#define Z7_DIAGNOSTIC_IGNORE_BEGIN_CAST_QUAL \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic ignored \"-Wcast-qual\"") +#define Z7_DIAGNOSTIC_IGNORE_END_CAST_QUAL \ + _Pragma("GCC diagnostic pop") +#else +#define Z7_DIAGNOSTIC_IGNORE_BEGIN_CAST_QUAL +#define Z7_DIAGNOSTIC_IGNORE_END_CAST_QUAL +#endif + +#define Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR(ptr, type, m, p) \ + Z7_DIAGNOSTIC_IGNORE_BEGIN_CAST_QUAL \ + type *p = Z7_CONTAINER_FROM_VTBL(ptr, type, m); \ + Z7_DIAGNOSTIC_IGNORE_END_CAST_QUAL + +#define Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR_pp_vt_p(type) \ + Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR(pp, type, vt, p) + + +// #define ZIP7_DECLARE_HANDLE(name) typedef void *name; +#define Z7_DECLARE_HANDLE(name) struct name##_dummy{int unused;}; typedef struct name##_dummy *name; + + +#define Z7_memset_0_ARRAY(a) memset((a), 0, sizeof(a)) + +#ifndef Z7_ARRAY_SIZE +#define Z7_ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#endif + + +#ifdef _WIN32 + +#define CHAR_PATH_SEPARATOR '\\' +#define WCHAR_PATH_SEPARATOR L'\\' +#define STRING_PATH_SEPARATOR "\\" +#define WSTRING_PATH_SEPARATOR L"\\" + +#else + +#define CHAR_PATH_SEPARATOR '/' +#define WCHAR_PATH_SEPARATOR L'/' +#define STRING_PATH_SEPARATOR "/" +#define WSTRING_PATH_SEPARATOR L"/" + +#endif + +#define k_PropVar_TimePrec_0 0 +#define k_PropVar_TimePrec_Unix 1 +#define k_PropVar_TimePrec_DOS 2 +#define k_PropVar_TimePrec_HighPrec 3 +#define k_PropVar_TimePrec_Base 16 +#define k_PropVar_TimePrec_100ns (k_PropVar_TimePrec_Base + 7) +#define k_PropVar_TimePrec_1ns (k_PropVar_TimePrec_Base + 9) + +EXTERN_C_END + +#endif + +/* +#ifndef Z7_ST +#ifdef _7ZIP_ST +#define Z7_ST +#endif +#endif +*/ diff --git a/libs/archive/thirdparty/C/7zWindows.h b/libs/archive/thirdparty/C/7zWindows.h new file mode 100644 index 0000000..42c6db8 --- /dev/null +++ b/libs/archive/thirdparty/C/7zWindows.h @@ -0,0 +1,101 @@ +/* 7zWindows.h -- StdAfx +2023-04-02 : Igor Pavlov : Public domain */ + +#ifndef ZIP7_INC_7Z_WINDOWS_H +#define ZIP7_INC_7Z_WINDOWS_H + +#ifdef _WIN32 + +#if defined(__clang__) +# pragma clang diagnostic push +#endif + +#if defined(_MSC_VER) + +#pragma warning(push) +#pragma warning(disable : 4668) // '_WIN32_WINNT' is not defined as a preprocessor macro, replacing with '0' for '#if/#elif' + +#if _MSC_VER == 1900 +// for old kit10 versions +// #pragma warning(disable : 4255) // winuser.h(13979): warning C4255: 'GetThreadDpiAwarenessContext': +#endif +// win10 Windows Kit: +#endif // _MSC_VER + +#if defined(_MSC_VER) && _MSC_VER <= 1200 && !defined(_WIN64) +// for msvc6 without sdk2003 +#define RPC_NO_WINDOWS_H +#endif + +#if defined(__MINGW32__) || defined(__MINGW64__) +// #if defined(__GNUC__) && !defined(__clang__) +#include +#else +#include +#endif +// #include +// #include + +// but if precompiled with clang-cl then we need +// #include +#if defined(_MSC_VER) +#pragma warning(pop) +#endif + +#if defined(__clang__) +# pragma clang diagnostic pop +#endif + +#if defined(_MSC_VER) && _MSC_VER <= 1200 && !defined(_WIN64) +#ifndef _W64 + +typedef long LONG_PTR, *PLONG_PTR; +typedef unsigned long ULONG_PTR, *PULONG_PTR; +typedef ULONG_PTR DWORD_PTR, *PDWORD_PTR; + +#define Z7_OLD_WIN_SDK +#endif // _W64 +#endif // _MSC_VER == 1200 + +#ifdef Z7_OLD_WIN_SDK + +#ifndef INVALID_FILE_ATTRIBUTES +#define INVALID_FILE_ATTRIBUTES ((DWORD)-1) +#endif +#ifndef INVALID_SET_FILE_POINTER +#define INVALID_SET_FILE_POINTER ((DWORD)-1) +#endif +#ifndef FILE_SPECIAL_ACCESS +#define FILE_SPECIAL_ACCESS (FILE_ANY_ACCESS) +#endif + +// ShlObj.h: +// #define BIF_NEWDIALOGSTYLE 0x0040 + +#pragma warning(disable : 4201) +// #pragma warning(disable : 4115) + +#undef VARIANT_TRUE +#define VARIANT_TRUE ((VARIANT_BOOL)-1) +#endif + +#endif // Z7_OLD_WIN_SDK + +#ifdef UNDER_CE +#undef VARIANT_TRUE +#define VARIANT_TRUE ((VARIANT_BOOL)-1) +#endif + + +#if defined(_MSC_VER) +#if _MSC_VER >= 1400 && _MSC_VER <= 1600 + // BaseTsd.h(148) : 'HandleToULong' : unreferenced inline function has been removed + // string.h + // #pragma warning(disable : 4514) +#endif +#endif + + +/* #include "7zTypes.h" */ + +#endif diff --git a/libs/archive/thirdparty/C/Compiler.h b/libs/archive/thirdparty/C/Compiler.h new file mode 100644 index 0000000..2a9c2b7 --- /dev/null +++ b/libs/archive/thirdparty/C/Compiler.h @@ -0,0 +1,236 @@ +/* Compiler.h : Compiler specific defines and pragmas +2024-01-22 : Igor Pavlov : Public domain */ + +#ifndef ZIP7_INC_COMPILER_H +#define ZIP7_INC_COMPILER_H + +#if defined(__clang__) +# define Z7_CLANG_VERSION (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) +#endif +#if defined(__clang__) && defined(__apple_build_version__) +# define Z7_APPLE_CLANG_VERSION Z7_CLANG_VERSION +#elif defined(__clang__) +# define Z7_LLVM_CLANG_VERSION Z7_CLANG_VERSION +#elif defined(__GNUC__) +# define Z7_GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) +#endif + +#ifdef _MSC_VER +#if !defined(__clang__) && !defined(__GNUC__) +#define Z7_MSC_VER_ORIGINAL _MSC_VER +#endif +#endif + +#if defined(__MINGW32__) || defined(__MINGW64__) +#define Z7_MINGW +#endif + +#if defined(__LCC__) && (defined(__MCST__) || defined(__e2k__)) +#define Z7_MCST_LCC +#define Z7_MCST_LCC_VERSION (__LCC__ * 100 + __LCC_MINOR__) +#endif + +/* +#if defined(__AVX2__) \ + || defined(Z7_GCC_VERSION) && (Z7_GCC_VERSION >= 40900) \ + || defined(Z7_APPLE_CLANG_VERSION) && (Z7_APPLE_CLANG_VERSION >= 40600) \ + || defined(Z7_LLVM_CLANG_VERSION) && (Z7_LLVM_CLANG_VERSION >= 30100) \ + || defined(Z7_MSC_VER_ORIGINAL) && (Z7_MSC_VER_ORIGINAL >= 1800) \ + || defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 1400) + #define Z7_COMPILER_AVX2_SUPPORTED + #endif +#endif +*/ + +// #pragma GCC diagnostic ignored "-Wunknown-pragmas" + +#ifdef __clang__ +// padding size of '' with 4 bytes to alignment boundary +#pragma GCC diagnostic ignored "-Wpadded" + +#if defined(Z7_LLVM_CLANG_VERSION) && (__clang_major__ == 13) \ + && defined(__FreeBSD__) +// freebsd: +#pragma GCC diagnostic ignored "-Wexcess-padding" +#endif + +#if __clang_major__ >= 16 +#pragma GCC diagnostic ignored "-Wunsafe-buffer-usage" +#endif + +#if __clang_major__ == 13 +#if defined(__SIZEOF_POINTER__) && (__SIZEOF_POINTER__ == 16) +// cheri +#pragma GCC diagnostic ignored "-Wcapability-to-integer-cast" +#endif +#endif + +#if __clang_major__ == 13 + // for + #pragma GCC diagnostic ignored "-Wreserved-identifier" +#endif + +#endif // __clang__ + +#if defined(_WIN32) && defined(__clang__) && __clang_major__ >= 16 +// #pragma GCC diagnostic ignored "-Wcast-function-type-strict" +#define Z7_DIAGNOSTIC_IGNORE_CAST_FUNCTION \ + _Pragma("GCC diagnostic ignored \"-Wcast-function-type-strict\"") +#else +#define Z7_DIAGNOSTIC_IGNORE_CAST_FUNCTION +#endif + +typedef void (*Z7_void_Function)(void); +#if defined(__clang__) || defined(__GNUC__) +#define Z7_CAST_FUNC_C (Z7_void_Function) +#elif defined(_MSC_VER) && _MSC_VER > 1920 +#define Z7_CAST_FUNC_C (void *) +// #pragma warning(disable : 4191) // 'type cast': unsafe conversion from 'FARPROC' to 'void (__cdecl *)()' +#else +#define Z7_CAST_FUNC_C +#endif +/* +#if (defined(__GNUC__) && (__GNUC__ >= 8)) || defined(__clang__) + // #pragma GCC diagnostic ignored "-Wcast-function-type" +#endif +*/ +#ifdef __GNUC__ +#if defined(Z7_GCC_VERSION) && (Z7_GCC_VERSION >= 40000) && (Z7_GCC_VERSION < 70000) +#pragma GCC diagnostic ignored "-Wstrict-aliasing" +#endif +#endif + + +#ifdef _MSC_VER + + #ifdef UNDER_CE + #define RPC_NO_WINDOWS_H + /* #pragma warning(disable : 4115) // '_RPC_ASYNC_STATE' : named type definition in parentheses */ + #pragma warning(disable : 4201) // nonstandard extension used : nameless struct/union + #pragma warning(disable : 4214) // nonstandard extension used : bit field types other than int + #endif + +#if defined(_MSC_VER) && _MSC_VER >= 1800 +#pragma warning(disable : 4464) // relative include path contains '..' +#endif + +// == 1200 : -O1 : for __forceinline +// >= 1900 : -O1 : for printf +#pragma warning(disable : 4710) // function not inlined + +#if _MSC_VER < 1900 +// winnt.h: 'Int64ShllMod32' +#pragma warning(disable : 4514) // unreferenced inline function has been removed +#endif + +#if _MSC_VER < 1300 +// #pragma warning(disable : 4702) // unreachable code +// Bra.c : -O1: +#pragma warning(disable : 4714) // function marked as __forceinline not inlined +#endif + +/* +#if _MSC_VER > 1400 && _MSC_VER <= 1900 +// strcat: This function or variable may be unsafe +// sysinfoapi.h: kit10: GetVersion was declared deprecated +#pragma warning(disable : 4996) +#endif +*/ + +#if _MSC_VER > 1200 +// -Wall warnings + +#pragma warning(disable : 4711) // function selected for automatic inline expansion +#pragma warning(disable : 4820) // '2' bytes padding added after data member + +#if _MSC_VER >= 1400 && _MSC_VER < 1920 +// 1400: string.h: _DBG_MEMCPY_INLINE_ +// 1600 - 191x : smmintrin.h __cplusplus' +// is not defined as a preprocessor macro, replacing with '0' for '#if/#elif' +#pragma warning(disable : 4668) + +// 1400 - 1600 : WinDef.h : 'FARPROC' : +// 1900 - 191x : immintrin.h: _readfsbase_u32 +// no function prototype given : converting '()' to '(void)' +#pragma warning(disable : 4255) +#endif + +#if _MSC_VER >= 1914 +// Compiler will insert Spectre mitigation for memory load if /Qspectre switch specified +#pragma warning(disable : 5045) +#endif + +#endif // _MSC_VER > 1200 +#endif // _MSC_VER + + +#if defined(__clang__) && (__clang_major__ >= 4) + #define Z7_PRAGMA_OPT_DISABLE_LOOP_UNROLL_VECTORIZE \ + _Pragma("clang loop unroll(disable)") \ + _Pragma("clang loop vectorize(disable)") + #define Z7_ATTRIB_NO_VECTORIZE +#elif defined(__GNUC__) && (__GNUC__ >= 5) \ + && (!defined(Z7_MCST_LCC_VERSION) || (Z7_MCST_LCC_VERSION >= 12610)) + #define Z7_ATTRIB_NO_VECTORIZE __attribute__((optimize("no-tree-vectorize"))) + // __attribute__((optimize("no-unroll-loops"))); + #define Z7_PRAGMA_OPT_DISABLE_LOOP_UNROLL_VECTORIZE +#elif defined(_MSC_VER) && (_MSC_VER >= 1920) + #define Z7_PRAGMA_OPT_DISABLE_LOOP_UNROLL_VECTORIZE \ + _Pragma("loop( no_vector )") + #define Z7_ATTRIB_NO_VECTORIZE +#else + #define Z7_PRAGMA_OPT_DISABLE_LOOP_UNROLL_VECTORIZE + #define Z7_ATTRIB_NO_VECTORIZE +#endif + +#if defined(MY_CPU_X86_OR_AMD64) && ( \ + defined(__clang__) && (__clang_major__ >= 4) \ + || defined(__GNUC__) && (__GNUC__ >= 5)) + #define Z7_ATTRIB_NO_SSE __attribute__((__target__("no-sse"))) +#else + #define Z7_ATTRIB_NO_SSE +#endif + +#define Z7_ATTRIB_NO_VECTOR \ + Z7_ATTRIB_NO_VECTORIZE \ + Z7_ATTRIB_NO_SSE + + +#if defined(__clang__) && (__clang_major__ >= 8) \ + || defined(__GNUC__) && (__GNUC__ >= 1000) \ + /* || defined(_MSC_VER) && (_MSC_VER >= 1920) */ + // GCC is not good for __builtin_expect() + #define Z7_LIKELY(x) (__builtin_expect((x), 1)) + #define Z7_UNLIKELY(x) (__builtin_expect((x), 0)) + // #define Z7_unlikely [[unlikely]] + // #define Z7_likely [[likely]] +#else + #define Z7_LIKELY(x) (x) + #define Z7_UNLIKELY(x) (x) + // #define Z7_likely +#endif + + +#if (defined(Z7_CLANG_VERSION) && (Z7_CLANG_VERSION >= 30600)) + +#if (Z7_CLANG_VERSION < 130000) +#define Z7_DIAGNOSTIC_IGNORE_BEGIN_RESERVED_MACRO_IDENTIFIER \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic ignored \"-Wreserved-id-macro\"") +#else +#define Z7_DIAGNOSTIC_IGNORE_BEGIN_RESERVED_MACRO_IDENTIFIER \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic ignored \"-Wreserved-macro-identifier\"") +#endif + +#define Z7_DIAGNOSTIC_IGNORE_END_RESERVED_MACRO_IDENTIFIER \ + _Pragma("GCC diagnostic pop") +#else +#define Z7_DIAGNOSTIC_IGNORE_BEGIN_RESERVED_MACRO_IDENTIFIER +#define Z7_DIAGNOSTIC_IGNORE_END_RESERVED_MACRO_IDENTIFIER +#endif + +#define UNUSED_VAR(x) (void)x; +/* #define UNUSED_VAR(x) x=x; */ + +#endif diff --git a/libs/archive/thirdparty/C/Precomp.h b/libs/archive/thirdparty/C/Precomp.h new file mode 100644 index 0000000..7747fdd --- /dev/null +++ b/libs/archive/thirdparty/C/Precomp.h @@ -0,0 +1,127 @@ +/* Precomp.h -- precompilation file +2024-01-25 : Igor Pavlov : Public domain */ + +#ifndef ZIP7_INC_PRECOMP_H +#define ZIP7_INC_PRECOMP_H + +/* + this file must be included before another *.h files and before . + this file is included from the following files: + C\*.c + C\Util\*\Precomp.h <- C\Util\*\*.c + CPP\Common\Common.h <- *\StdAfx.h <- *\*.cpp + + this file can set the following macros: + Z7_LARGE_PAGES 1 + Z7_LONG_PATH 1 + Z7_WIN32_WINNT_MIN 0x0500 (or higher) : we require at least win2000+ for 7-Zip + _WIN32_WINNT 0x0500 (or higher) + WINVER _WIN32_WINNT + UNICODE 1 + _UNICODE 1 +*/ + +#include "Compiler.h" + +#ifdef _MSC_VER +// #pragma warning(disable : 4206) // nonstandard extension used : translation unit is empty +#if _MSC_VER >= 1912 +// #pragma warning(disable : 5039) // pointer or reference to potentially throwing function passed to 'extern "C"' function under - EHc.Undefined behavior may occur if this function throws an exception. +#endif +#endif + +/* +// for debug: +#define UNICODE 1 +#define _UNICODE 1 +#define _WIN32_WINNT 0x0500 // win2000 +#ifndef WINVER + #define WINVER _WIN32_WINNT +#endif +*/ + +#ifdef _WIN32 +/* + this "Precomp.h" file must be included before , + if we want to define _WIN32_WINNT before . +*/ + +#ifndef Z7_LARGE_PAGES +#ifndef Z7_NO_LARGE_PAGES +#define Z7_LARGE_PAGES 1 +#endif +#endif + +#ifndef Z7_LONG_PATH +#ifndef Z7_NO_LONG_PATH +#define Z7_LONG_PATH 1 +#endif +#endif + +#ifndef Z7_DEVICE_FILE +#ifndef Z7_NO_DEVICE_FILE +// #define Z7_DEVICE_FILE 1 +#endif +#endif + +// we don't change macros if included after +#ifndef _WINDOWS_ + +#ifndef Z7_WIN32_WINNT_MIN + #if defined(_M_ARM64) || defined(__aarch64__) + // #define Z7_WIN32_WINNT_MIN 0x0a00 // win10 + #define Z7_WIN32_WINNT_MIN 0x0600 // vista + #elif defined(_M_ARM) && defined(_M_ARMT) && defined(_M_ARM_NT) + // #define Z7_WIN32_WINNT_MIN 0x0602 // win8 + #define Z7_WIN32_WINNT_MIN 0x0600 // vista + #elif defined(_M_X64) || defined(_M_AMD64) || defined(__x86_64__) || defined(_M_IA64) + #define Z7_WIN32_WINNT_MIN 0x0503 // win2003 + // #elif defined(_M_IX86) || defined(__i386__) + // #define Z7_WIN32_WINNT_MIN 0x0500 // win2000 + #else // x86 and another(old) systems + #define Z7_WIN32_WINNT_MIN 0x0500 // win2000 + // #define Z7_WIN32_WINNT_MIN 0x0502 // win2003 // for debug + #endif +#endif // Z7_WIN32_WINNT_MIN + + +#ifndef Z7_DO_NOT_DEFINE_WIN32_WINNT +#ifdef _WIN32_WINNT + // #error Stop_Compiling_Bad_WIN32_WINNT +#else + #ifndef Z7_NO_DEFINE_WIN32_WINNT +Z7_DIAGNOSTIC_IGNORE_BEGIN_RESERVED_MACRO_IDENTIFIER + #define _WIN32_WINNT Z7_WIN32_WINNT_MIN +Z7_DIAGNOSTIC_IGNORE_END_RESERVED_MACRO_IDENTIFIER + #endif +#endif // _WIN32_WINNT + +#ifndef WINVER + #define WINVER _WIN32_WINNT +#endif +#endif // Z7_DO_NOT_DEFINE_WIN32_WINNT + + +#ifndef _MBCS +#ifndef Z7_NO_UNICODE +// UNICODE and _UNICODE are used by and by 7-zip code. + +#ifndef UNICODE +#define UNICODE 1 +#endif + +#ifndef _UNICODE +Z7_DIAGNOSTIC_IGNORE_BEGIN_RESERVED_MACRO_IDENTIFIER +#define _UNICODE 1 +Z7_DIAGNOSTIC_IGNORE_END_RESERVED_MACRO_IDENTIFIER +#endif + +#endif // Z7_NO_UNICODE +#endif // _MBCS +#endif // _WINDOWS_ + +// #include "7zWindows.h" + +#endif // _WIN32 + +#endif diff --git a/libs/archive/thirdparty/CPP/7zip/Archive/IArchive.h b/libs/archive/thirdparty/CPP/7zip/Archive/IArchive.h new file mode 100644 index 0000000..a817015 --- /dev/null +++ b/libs/archive/thirdparty/CPP/7zip/Archive/IArchive.h @@ -0,0 +1,754 @@ +// IArchive.h + +#ifndef ZIP7_INC_IARCHIVE_H +#define ZIP7_INC_IARCHIVE_H + +#include "../IProgress.h" +#include "../IStream.h" +#include "../PropID.h" + +Z7_PURE_INTERFACES_BEGIN + + +#define Z7_IFACE_CONSTR_ARCHIVE_SUB(i, base, n) \ + Z7_DECL_IFACE_7ZIP_SUB(i, base, 6, n) \ + { Z7_IFACE_COM7_PURE(i) }; + +#define Z7_IFACE_CONSTR_ARCHIVE(i, n) \ + Z7_IFACE_CONSTR_ARCHIVE_SUB(i, IUnknown, n) + +/* +How the function in 7-Zip returns object for output parameter via pointer + +1) The caller sets the value of variable before function call: + PROPVARIANT : vt = VT_EMPTY + BSTR : NULL + IUnknown* and derived interfaces : NULL + another scalar types : any non-initialized value is allowed + +2) The callee in current 7-Zip code now can free input object for output parameter: + PROPVARIANT : the callee calls VariantClear(propvaiant_ptr) for input + value stored in variable + another types : the callee ignores stored value. + +3) The callee writes new value to variable for output parameter and + returns execution to caller. + +4) The caller must free or release object returned by the callee: + PROPVARIANT : VariantClear(&propvaiant) + BSTR : SysFreeString(bstr) + IUnknown* and derived interfaces : if (ptr) ptr->Relase() +*/ + + +namespace NFileTimeType +{ + enum EEnum + { + kNotDefined = -1, + kWindows = 0, + kUnix, + kDOS, + k1ns + }; +} + +namespace NArcInfoFlags +{ + const UInt32 kKeepName = 1 << 0; // keep name of file in archive name + const UInt32 kAltStreams = 1 << 1; // the handler supports alt streams + const UInt32 kNtSecure = 1 << 2; // the handler supports NT security + const UInt32 kFindSignature = 1 << 3; // the handler can find start of archive + const UInt32 kMultiSignature = 1 << 4; // there are several signatures + const UInt32 kUseGlobalOffset = 1 << 5; // the seek position of stream must be set as global offset + const UInt32 kStartOpen = 1 << 6; // call handler for each start position + const UInt32 kPureStartOpen = 1 << 7; // call handler only for start of file + const UInt32 kBackwardOpen = 1 << 8; // archive can be open backward + const UInt32 kPreArc = 1 << 9; // such archive can be stored before real archive (like SFX stub) + const UInt32 kSymLinks = 1 << 10; // the handler supports symbolic links + const UInt32 kHardLinks = 1 << 11; // the handler supports hard links + const UInt32 kByExtOnlyOpen = 1 << 12; // call handler only if file extension matches + const UInt32 kHashHandler = 1 << 13; // the handler contains the hashes (checksums) + const UInt32 kCTime = 1 << 14; + const UInt32 kCTime_Default = 1 << 15; + const UInt32 kATime = 1 << 16; + const UInt32 kATime_Default = 1 << 17; + const UInt32 kMTime = 1 << 18; + const UInt32 kMTime_Default = 1 << 19; + // const UInt32 kTTime_Reserved = 1 << 20; + // const UInt32 kTTime_Reserved_Default = 1 << 21; +} + +namespace NArcInfoTimeFlags +{ + const unsigned kTime_Prec_Mask_bit_index = 0; + const unsigned kTime_Prec_Mask_num_bits = 26; + + const unsigned kTime_Prec_Default_bit_index = 27; + const unsigned kTime_Prec_Default_num_bits = 5; +} + +#define TIME_PREC_TO_ARC_FLAGS_MASK(v) \ + ((UInt32)1 << (NArcInfoTimeFlags::kTime_Prec_Mask_bit_index + (v))) + +#define TIME_PREC_TO_ARC_FLAGS_TIME_DEFAULT(v) \ + ((UInt32)(v) << NArcInfoTimeFlags::kTime_Prec_Default_bit_index) + +namespace NArchive +{ + namespace NHandlerPropID + { + enum + { + kName = 0, // VT_BSTR + kClassID, // binary GUID in VT_BSTR + kExtension, // VT_BSTR + kAddExtension, // VT_BSTR + kUpdate, // VT_BOOL + kKeepName, // VT_BOOL + kSignature, // binary in VT_BSTR + kMultiSignature, // binary in VT_BSTR + kSignatureOffset, // VT_UI4 + kAltStreams, // VT_BOOL + kNtSecure, // VT_BOOL + kFlags, // VT_UI4 + kTimeFlags // VT_UI4 + }; + } + + namespace NExtract + { + namespace NAskMode + { + enum + { + kExtract = 0, + kTest, + kSkip, + kReadExternal + }; + } + + namespace NOperationResult + { + enum + { + kOK = 0, + kUnsupportedMethod, + kDataError, + kCRCError, + kUnavailable, + kUnexpectedEnd, + kDataAfterEnd, + kIsNotArc, + kHeadersError, + kWrongPassword + // , kMemError + }; + } + } + + namespace NEventIndexType + { + enum + { + kNoIndex = 0, + kInArcIndex, + kBlockIndex, + kOutArcIndex + // kArcProp + }; + } + + namespace NUpdate + { + namespace NOperationResult + { + enum + { + kOK = 0 + // kError = 1, + // kError_FileChanged + }; + } + } +} + +#define Z7_IFACEM_IArchiveOpenCallback(x) \ + x(SetTotal(const UInt64 *files, const UInt64 *bytes)) \ + x(SetCompleted(const UInt64 *files, const UInt64 *bytes)) \ + +Z7_IFACE_CONSTR_ARCHIVE(IArchiveOpenCallback, 0x10) + +/* +IArchiveExtractCallback:: + +7-Zip doesn't call IArchiveExtractCallback functions + GetStream() + PrepareOperation() + SetOperationResult() +from different threads simultaneously. +But 7-Zip can call functions for IProgress or ICompressProgressInfo functions +from another threads simultaneously with calls for IArchiveExtractCallback interface. + +IArchiveExtractCallback::GetStream() + UInt32 index - index of item in Archive + Int32 askExtractMode (Extract::NAskMode) + if (askMode != NExtract::NAskMode::kExtract) + { + then the callee doesn't write data to stream: (*outStream == NULL) + } + + Out: + (*outStream == NULL) - for directories + (*outStream == NULL) - if link (hard link or symbolic link) was created + if (*outStream == NULL && askMode == NExtract::NAskMode::kExtract) + { + then the caller must skip extracting of that file. + } + + returns: + S_OK : OK + S_FALSE : data error (for decoders) + +if (IProgress::SetTotal() was called) +{ + IProgress::SetCompleted(completeValue) uses + packSize - for some stream formats (xz, gz, bz2, lzma, z, ppmd). + unpackSize - for another formats. +} +else +{ + IProgress::SetCompleted(completeValue) uses packSize. +} + +SetOperationResult() + 7-Zip calls SetOperationResult at the end of extracting, + so the callee can close the file, set attributes, timestamps and security information. + + Int32 opRes (NExtract::NOperationResult) +*/ + +// INTERFACE_IProgress(x) + +#define Z7_IFACEM_IArchiveExtractCallback(x) \ + x(GetStream(UInt32 index, ISequentialOutStream **outStream, Int32 askExtractMode)) \ + x(PrepareOperation(Int32 askExtractMode)) \ + x(SetOperationResult(Int32 opRes)) \ + +Z7_IFACE_CONSTR_ARCHIVE_SUB(IArchiveExtractCallback, IProgress, 0x20) + + + +/* +v23: +IArchiveExtractCallbackMessage2 can be requested from IArchiveExtractCallback object + by Extract() or UpdateItems() functions to report about extracting errors +ReportExtractResult() + UInt32 indexType (NEventIndexType) + UInt32 index + Int32 opRes (NExtract::NOperationResult) +*/ +/* +before v23: +#define Z7_IFACEM_IArchiveExtractCallbackMessage(x) \ + x(ReportExtractResult(UInt32 indexType, UInt32 index, Int32 opRes)) +Z7_IFACE_CONSTR_ARCHIVE_SUB(IArchiveExtractCallbackMessage, IProgress, 0x21) +*/ +#define Z7_IFACEM_IArchiveExtractCallbackMessage2(x) \ + x(ReportExtractResult(UInt32 indexType, UInt32 index, Int32 opRes)) +Z7_IFACE_CONSTR_ARCHIVE(IArchiveExtractCallbackMessage2, 0x22) + +#define Z7_IFACEM_IArchiveOpenVolumeCallback(x) \ + x(GetProperty(PROPID propID, PROPVARIANT *value)) \ + x(GetStream(const wchar_t *name, IInStream **inStream)) +Z7_IFACE_CONSTR_ARCHIVE(IArchiveOpenVolumeCallback, 0x30) + + +#define Z7_IFACEM_IInArchiveGetStream(x) \ + x(GetStream(UInt32 index, ISequentialInStream **stream)) +Z7_IFACE_CONSTR_ARCHIVE(IInArchiveGetStream, 0x40) + +#define Z7_IFACEM_IArchiveOpenSetSubArchiveName(x) \ + x(SetSubArchiveName(const wchar_t *name)) +Z7_IFACE_CONSTR_ARCHIVE(IArchiveOpenSetSubArchiveName, 0x50) + + +/* +IInArchive::Open + stream + if (kUseGlobalOffset), stream current position can be non 0. + if (!kUseGlobalOffset), stream current position is 0. + if (maxCheckStartPosition == NULL), the handler can try to search archive start in stream + if (*maxCheckStartPosition == 0), the handler must check only current position as archive start + +IInArchive::Extract: + indices must be sorted + numItems = (UInt32)(Int32)-1 = 0xFFFFFFFF means "all files" + testMode != 0 means "test files without writing to outStream" + +IInArchive::GetArchiveProperty: + kpidOffset - start offset of archive. + VT_EMPTY : means offset = 0. + VT_UI4, VT_UI8, VT_I8 : result offset; negative values is allowed + kpidPhySize - size of archive. VT_EMPTY means unknown size. + kpidPhySize is allowed to be larger than file size. In that case it must show + supposed size. + + kpidIsDeleted: + kpidIsAltStream: + kpidIsAux: + kpidINode: + must return VARIANT_TRUE (VT_BOOL), if archive can support that property in GetProperty. + + +Notes: + Don't call IInArchive functions for same IInArchive object from different threads simultaneously. + Some IInArchive handlers will work incorrectly in that case. +*/ + +#if defined(_MSC_VER) && !defined(__clang__) + #define MY_NO_THROW_DECL_ONLY Z7_COM7F_E +#else + #define MY_NO_THROW_DECL_ONLY +#endif + +#define Z7_IFACEM_IInArchive(x) \ + x(Open(IInStream *stream, const UInt64 *maxCheckStartPosition, IArchiveOpenCallback *openCallback)) \ + x(Close()) \ + x(GetNumberOfItems(UInt32 *numItems)) \ + x(GetProperty(UInt32 index, PROPID propID, PROPVARIANT *value)) \ + x(Extract(const UInt32 *indices, UInt32 numItems, Int32 testMode, IArchiveExtractCallback *extractCallback)) \ + x(GetArchiveProperty(PROPID propID, PROPVARIANT *value)) \ + x(GetNumberOfProperties(UInt32 *numProps)) \ + x(GetPropertyInfo(UInt32 index, BSTR *name, PROPID *propID, VARTYPE *varType)) \ + x(GetNumberOfArchiveProperties(UInt32 *numProps)) \ + x(GetArchivePropertyInfo(UInt32 index, BSTR *name, PROPID *propID, VARTYPE *varType)) \ + +Z7_IFACE_CONSTR_ARCHIVE(IInArchive, 0x60) + +namespace NParentType +{ + enum + { + kDir = 0, + kAltStream + }; +} + +namespace NPropDataType +{ + const UInt32 kMask_ZeroEnd = 1 << 4; + // const UInt32 kMask_BigEndian = 1 << 5; + const UInt32 kMask_Utf = 1 << 6; + const UInt32 kMask_Utf8 = kMask_Utf | 0; + const UInt32 kMask_Utf16 = kMask_Utf | 1; + // const UInt32 kMask_Utf32 = kMask_Utf | 2; + + const UInt32 kNotDefined = 0; + const UInt32 kRaw = 1; + + const UInt32 kUtf8z = kMask_Utf8 | kMask_ZeroEnd; + const UInt32 kUtf16z = kMask_Utf16 | kMask_ZeroEnd; +} + +// UTF string (pointer to wchar_t) with zero end and little-endian. +#define PROP_DATA_TYPE_wchar_t_PTR_Z_LE ((NPropDataType::kMask_Utf | NPropDataType::kMask_ZeroEnd) + (sizeof(wchar_t) >> 1)) + + +/* +GetRawProp: + Result: + S_OK - even if property is not set +*/ + +#define Z7_IFACEM_IArchiveGetRawProps(x) \ + x(GetParent(UInt32 index, UInt32 *parent, UInt32 *parentType)) \ + x(GetRawProp(UInt32 index, PROPID propID, const void **data, UInt32 *dataSize, UInt32 *propType)) \ + x(GetNumRawProps(UInt32 *numProps)) \ + x(GetRawPropInfo(UInt32 index, BSTR *name, PROPID *propID)) + +Z7_IFACE_CONSTR_ARCHIVE(IArchiveGetRawProps, 0x70) + +#define Z7_IFACEM_IArchiveGetRootProps(x) \ + x(GetRootProp(PROPID propID, PROPVARIANT *value)) \ + x(GetRootRawProp(PROPID propID, const void **data, UInt32 *dataSize, UInt32 *propType)) \ + +Z7_IFACE_CONSTR_ARCHIVE(IArchiveGetRootProps, 0x71) + +#define Z7_IFACEM_IArchiveOpenSeq(x) \ + x(OpenSeq(ISequentialInStream *stream)) \ + +Z7_IFACE_CONSTR_ARCHIVE(IArchiveOpenSeq, 0x61) + +/* + OpenForSize + Result: + S_FALSE - is not archive + ? - DATA error +*/ + +/* +const UInt32 kOpenFlags_RealPhySize = 1 << 0; +const UInt32 kOpenFlags_NoSeek = 1 << 1; +// const UInt32 kOpenFlags_BeforeExtract = 1 << 2; +*/ + +/* +Flags: + 0 - opens archive with IInStream, if IInStream interface is supported + - if phySize is not available, it doesn't try to make full parse to get phySize + kOpenFlags_NoSeek - ArcOpen2 function doesn't use IInStream interface, even if it's available + kOpenFlags_RealPhySize - the handler will try to get PhySize, even if it requires full decompression for file + + if handler is not allowed to use IInStream and the flag kOpenFlags_RealPhySize is not specified, + the handler can return S_OK, but it doesn't check even Signature. + So next Extract can be called for that sequential stream. +*/ +/* +#define Z7_IFACEM_IArchiveOpen2(x) \ + x(ArcOpen2(ISequentialInStream *stream, UInt32 flags, IArchiveOpenCallback *openCallback)) +Z7_IFACE_CONSTR_ARCHIVE(IArchiveOpen2, 0x62) +*/ + +// ---------- UPDATE ---------- + +/* +GetUpdateItemInfo outs: +*newData *newProps + 0 0 - Copy data and properties from archive + 0 1 - Copy data from archive, request new properties + 1 0 - that combination is unused now + 1 1 - Request new data and new properties. It can be used even for folders + + indexInArchive = -1 if there is no item in archive, or if it doesn't matter. + + +GetStream out: + Result: + S_OK: + (*inStream == NULL) - only for directories + - the bug was fixed in 9.33: (*Stream == NULL) was in case of anti-file + (*inStream != NULL) - for any file, even for empty file or anti-file + S_FALSE - skip that file (don't add item to archive) - (client code can't open stream of that file by some reason) + (*inStream == NULL) + +The order of calling for hard links: + - GetStream() + - GetProperty(kpidHardLink) + +SetOperationResult() + Int32 opRes (NExtract::NOperationResult::kOK) +*/ + +// INTERFACE_IProgress(x) +#define Z7_IFACEM_IArchiveUpdateCallback(x) \ + x(GetUpdateItemInfo(UInt32 index, Int32 *newData, Int32 *newProps, UInt32 *indexInArchive)) \ + x(GetProperty(UInt32 index, PROPID propID, PROPVARIANT *value)) \ + x(GetStream(UInt32 index, ISequentialInStream **inStream)) \ + x(SetOperationResult(Int32 operationResult)) \ + +Z7_IFACE_CONSTR_ARCHIVE_SUB(IArchiveUpdateCallback, IProgress, 0x80) + +// INTERFACE_IArchiveUpdateCallback(x) +#define Z7_IFACEM_IArchiveUpdateCallback2(x) \ + x(GetVolumeSize(UInt32 index, UInt64 *size)) \ + x(GetVolumeStream(UInt32 index, ISequentialOutStream **volumeStream)) \ + +Z7_IFACE_CONSTR_ARCHIVE_SUB(IArchiveUpdateCallback2, IArchiveUpdateCallback, 0x82) + +namespace NUpdateNotifyOp +{ + enum + { + kAdd = 0, + kUpdate, + kAnalyze, + kReplicate, + kRepack, + kSkip, + kDelete, + kHeader, + kHashRead, + kInFileChanged + // , kOpFinished + // , kNumDefined + }; +} + +/* +IArchiveUpdateCallbackFile::ReportOperation + UInt32 indexType (NEventIndexType) + UInt32 index + UInt32 notifyOp (NUpdateNotifyOp) +*/ + +#define Z7_IFACEM_IArchiveUpdateCallbackFile(x) \ + x(GetStream2(UInt32 index, ISequentialInStream **inStream, UInt32 notifyOp)) \ + x(ReportOperation(UInt32 indexType, UInt32 index, UInt32 notifyOp)) \ + +Z7_IFACE_CONSTR_ARCHIVE(IArchiveUpdateCallbackFile, 0x83) + + +#define Z7_IFACEM_IArchiveGetDiskProperty(x) \ + x(GetDiskProperty(UInt32 index, PROPID propID, PROPVARIANT *value)) \ + +Z7_IFACE_CONSTR_ARCHIVE(IArchiveGetDiskProperty, 0x84) + +/* +#define Z7_IFACEM_IArchiveUpdateCallbackArcProp(x) \ + x(ReportProp(UInt32 indexType, UInt32 index, PROPID propID, const PROPVARIANT *value)) \ + x(ReportRawProp(UInt32 indexType, UInt32 index, PROPID propID, const void *data, UInt32 dataSize, UInt32 propType)) \ + x(ReportFinished(UInt32 indexType, UInt32 index, Int32 opRes)) \ + x(DoNeedArcProp(PROPID propID, Int32 *answer)) \ + +Z7_IFACE_CONSTR_ARCHIVE(IArchiveUpdateCallbackArcProp, 0x85) +*/ + +/* +UpdateItems() +------------- + + outStream: output stream. (the handler) MUST support the case when + Seek position in outStream is not ZERO. + but the caller calls with empty outStream and seek position is ZERO?? + + archives with stub: + + If archive is open and the handler and (Offset > 0), then the handler + knows about stub size. + UpdateItems(): + 1) the handler MUST copy that stub to outStream + 2) the caller MUST NOT copy the stub to outStream, if + "rsfx" property is set with SetProperties + + the handler must support the case where + ISequentialOutStream *outStream +*/ + + +#define Z7_IFACEM_IOutArchive(x) \ + x(UpdateItems(ISequentialOutStream *outStream, UInt32 numItems, IArchiveUpdateCallback *updateCallback)) \ + x(GetFileTimeType(UInt32 *type)) + +Z7_IFACE_CONSTR_ARCHIVE(IOutArchive, 0xA0) + + +/* +ISetProperties::SetProperties() + PROPVARIANT values[i].vt: + VT_EMPTY + VT_BOOL + VT_UI4 - if 32-bit number + VT_UI8 - if 64-bit number + VT_BSTR +*/ + +#define Z7_IFACEM_ISetProperties(x) \ + x(SetProperties(const wchar_t * const *names, const PROPVARIANT *values, UInt32 numProps)) + +Z7_IFACE_CONSTR_ARCHIVE(ISetProperties, 0x03) + +#define Z7_IFACEM_IArchiveKeepModeForNextOpen(x) \ + x(KeepModeForNextOpen()) \ + +Z7_IFACE_CONSTR_ARCHIVE(IArchiveKeepModeForNextOpen, 0x04) + +/* Exe handler: the handler for executable format (PE, ELF, Mach-O). + SFX archive: executable stub + some tail data. + before 9.31: exe handler didn't parse SFX archives as executable format. + for 9.31+: exe handler parses SFX archives as executable format, only if AllowTail(1) was called */ + +#define Z7_IFACEM_IArchiveAllowTail(x) \ + x(AllowTail(Int32 allowTail)) \ + +Z7_IFACE_CONSTR_ARCHIVE(IArchiveAllowTail, 0x05) + + +namespace NRequestMemoryUseFlags +{ + const UInt32 k_AllowedSize_WasForced = 1 << 0; // (*allowedSize) was forced by -mmemx or -smemx + const UInt32 k_DefaultLimit_Exceeded = 1 << 1; // default limit of archive format was exceeded + const UInt32 k_MLimit_Exceeded = 1 << 2; // -mmemx value was exceeded + const UInt32 k_SLimit_Exceeded = 1 << 3; // -smemx value was exceeded + + const UInt32 k_NoErrorMessage = 1 << 10; // do not show error message, and show only request + const UInt32 k_IsReport = 1 << 11; // only report is required, without user request + + const UInt32 k_SkipArc_IsExpected = 1 << 12; // NRequestMemoryAnswerFlags::k_SkipArc flag answer is expected + const UInt32 k_Report_SkipArc = 1 << 13; // report about SkipArc operation + + // const UInt32 k_SkipBigFile_IsExpected = 1 << 14; // NRequestMemoryAnswerFlags::k_SkipBigFiles flag answer is expected (unused) + // const UInt32 k_Report_SkipBigFile = 1 << 15; // report about SkipFile operation (unused) + + // const UInt32 k_SkipBigFiles_IsExpected = 1 << 16; // NRequestMemoryAnswerFlags::k_SkipBigFiles flag answer is expected (unused) + // const UInt32 k_Report_SkipBigFiles = 1 << 17; // report that all big files will be skipped (unused) +} + +namespace NRequestMemoryAnswerFlags +{ + const UInt32 k_Allow = 1 << 0; // allow further archive extraction + const UInt32 k_Stop = 1 << 1; // for exit (and return_code == E_ABORT is used) + const UInt32 k_SkipArc = 1 << 2; // skip current archive extraction + // const UInt32 k_SkipBigFile = 1 << 4; // skip extracting of files that exceed limit (unused) + // const UInt32 k_SkipBigFiles = 1 << 5; // skip extracting of files that exceed limit (unused) + const UInt32 k_Limit_Exceeded = 1 << 10; // limit was exceeded +} + +/* + *allowedSize is in/out: + in : default allowed memory usage size or forced size, if it was changed by switch -mmemx. + out : value specified by user or unchanged value. + + *answerFlags is in/out: + *answerFlags must be set by caller before calling for default action, + + indexType : must be set with NEventIndexType::* constant + (indexType == kNoIndex), if request for whole archive. + index : must be set for some (indexType) types (if + fileIndex , if (indexType == NEventIndexType::kInArcIndex) + 0, if if (indexType == kNoIndex) + path : NULL can be used for any indexType. +*/ +#define Z7_IFACEM_IArchiveRequestMemoryUseCallback(x) \ + x(RequestMemoryUse(UInt32 flags, UInt32 indexType, UInt32 index, const wchar_t *path, \ + UInt64 requiredSize, UInt64 *allowedSize, UInt32 *answerFlags)) +Z7_IFACE_CONSTR_ARCHIVE(IArchiveRequestMemoryUseCallback, 0x09) + + +struct CStatProp +{ + const char *Name; + UInt32 PropID; + VARTYPE vt; +}; + +namespace NWindows { +namespace NCOM { +// PropVariant.cpp +BSTR AllocBstrFromAscii(const char *s) throw(); +}} + + +#define IMP_IInArchive_GetProp_Base(fn, f, k) \ + Z7_COM7F_IMF(CHandler::fn(UInt32 *numProps)) \ + { *numProps = Z7_ARRAY_SIZE(k); return S_OK; } \ + Z7_COM7F_IMF(CHandler::f(UInt32 index, BSTR *name, PROPID *propID, VARTYPE *varType)) \ + { if (index >= Z7_ARRAY_SIZE(k)) return E_INVALIDARG; \ + +#define IMP_IInArchive_GetProp_NO_NAME(fn, f, k) \ + IMP_IInArchive_GetProp_Base(fn, f, k) \ + *propID = k[index]; \ + *varType = k7z_PROPID_To_VARTYPE[(unsigned)*propID]; \ + *name = NULL; return S_OK; } \ + +#define IMP_IInArchive_GetProp_WITH_NAME(fn, f, k) \ + IMP_IInArchive_GetProp_Base(fn, f, k) \ + const CStatProp &prop = k[index]; \ + *propID = (PROPID)prop.PropID; \ + *varType = prop.vt; \ + *name = NWindows::NCOM::AllocBstrFromAscii(prop.Name); return S_OK; } \ + + +#define IMP_IInArchive_Props \ + IMP_IInArchive_GetProp_NO_NAME(GetNumberOfProperties, GetPropertyInfo, kProps) + +#define IMP_IInArchive_Props_WITH_NAME \ + IMP_IInArchive_GetProp_WITH_NAME(GetNumberOfProperties, GetPropertyInfo, kProps) + +#define IMP_IInArchive_ArcProps \ + IMP_IInArchive_GetProp_NO_NAME(GetNumberOfArchiveProperties, GetArchivePropertyInfo, kArcProps) + +#define IMP_IInArchive_ArcProps_WITH_NAME \ + IMP_IInArchive_GetProp_WITH_NAME(GetNumberOfArchiveProperties, GetArchivePropertyInfo, kArcProps) + +#define IMP_IInArchive_ArcProps_NO_Table \ + Z7_COM7F_IMF(CHandler::GetNumberOfArchiveProperties(UInt32 *numProps)) \ + { *numProps = 0; return S_OK; } \ + Z7_COM7F_IMF(CHandler::GetArchivePropertyInfo(UInt32, BSTR *, PROPID *, VARTYPE *)) \ + { return E_NOTIMPL; } \ + +#define IMP_IInArchive_ArcProps_NO \ + IMP_IInArchive_ArcProps_NO_Table \ + Z7_COM7F_IMF(CHandler::GetArchiveProperty(PROPID, PROPVARIANT *value)) \ + { value->vt = VT_EMPTY; return S_OK; } + + +#define Z7_class_CHandler_final \ + Z7_class_final(CHandler) + + +#define Z7_CLASS_IMP_CHandler_IInArchive_0 \ + Z7_CLASS_IMP_COM_1(CHandler, IInArchive) +#define Z7_CLASS_IMP_CHandler_IInArchive_1(i1) \ + Z7_CLASS_IMP_COM_2(CHandler, IInArchive, i1) +#define Z7_CLASS_IMP_CHandler_IInArchive_2(i1, i2) \ + Z7_CLASS_IMP_COM_3(CHandler, IInArchive, i1, i2) +#define Z7_CLASS_IMP_CHandler_IInArchive_3(i1, i2, i3) \ + Z7_CLASS_IMP_COM_4(CHandler, IInArchive, i1, i2, i3) +#define Z7_CLASS_IMP_CHandler_IInArchive_4(i1, i2, i3, i4) \ + Z7_CLASS_IMP_COM_5(CHandler, IInArchive, i1, i2, i3, i4) +#define Z7_CLASS_IMP_CHandler_IInArchive_5(i1, i2, i3, i4, i5) \ + Z7_CLASS_IMP_COM_6(CHandler, IInArchive, i1, i2, i3, i4, i5) + + + +#define k_IsArc_Res_NO 0 +#define k_IsArc_Res_YES 1 +#define k_IsArc_Res_NEED_MORE 2 +// #define k_IsArc_Res_YES_LOW_PROB 3 + +#define API_FUNC_IsArc EXTERN_C UInt32 WINAPI +#define API_FUNC_static_IsArc extern "C" { static UInt32 WINAPI + +extern "C" +{ + typedef HRESULT (WINAPI *Func_CreateObject)(const GUID *clsID, const GUID *iid, void **outObject); + + typedef UInt32 (WINAPI *Func_IsArc)(const Byte *p, size_t size); + typedef HRESULT (WINAPI *Func_GetIsArc)(UInt32 formatIndex, Func_IsArc *isArc); + + typedef HRESULT (WINAPI *Func_GetNumberOfFormats)(UInt32 *numFormats); + typedef HRESULT (WINAPI *Func_GetHandlerProperty)(PROPID propID, PROPVARIANT *value); + typedef HRESULT (WINAPI *Func_GetHandlerProperty2)(UInt32 index, PROPID propID, PROPVARIANT *value); + + typedef HRESULT (WINAPI *Func_SetCaseSensitive)(Int32 caseSensitive); + typedef HRESULT (WINAPI *Func_SetLargePageMode)(); + // typedef HRESULT (WINAPI *Func_SetClientVersion)(UInt32 version); + + typedef IOutArchive * (*Func_CreateOutArchive)(); + typedef IInArchive * (*Func_CreateInArchive)(); +} + + +/* + if there is no time in archive, external MTime of archive + will be used instead of _item.Time from archive. + For 7-zip before 22.00 we need to return some supported value. + But (kpidTimeType > kDOS) is not allowed in 7-Zip before 22.00. + So we return highest precision value supported by old 7-Zip. + new 7-Zip 22.00 doesn't use that value in usual cases. +*/ + + +#define DECLARE_AND_SET_CLIENT_VERSION_VAR +#define GET_FileTimeType_NotDefined_for_GetFileTimeType \ + NFileTimeType::kWindows + +/* +extern UInt32 g_ClientVersion; + +#define GET_CLIENT_VERSION(major, minor) \ + ((UInt32)(((UInt32)(major) << 16) | (UInt32)(minor))) + +#define DECLARE_AND_SET_CLIENT_VERSION_VAR \ + UInt32 g_ClientVersion = GET_CLIENT_VERSION(MY_VER_MAJOR, MY_VER_MINOR); + +#define GET_FileTimeType_NotDefined_for_GetFileTimeType \ + ((UInt32)(g_ClientVersion >= GET_CLIENT_VERSION(22, 0) ? \ + (UInt32)(Int32)NFileTimeType::kNotDefined : \ + NFileTimeType::kWindows)) +*/ + +Z7_PURE_INTERFACES_END +#endif diff --git a/libs/archive/thirdparty/CPP/7zip/ICoder.h b/libs/archive/thirdparty/CPP/7zip/ICoder.h new file mode 100644 index 0000000..aec2834 --- /dev/null +++ b/libs/archive/thirdparty/CPP/7zip/ICoder.h @@ -0,0 +1,477 @@ +// ICoder.h + +#ifndef ZIP7_INC_ICODER_H +#define ZIP7_INC_ICODER_H + +#include "IStream.h" + +Z7_PURE_INTERFACES_BEGIN + +#define Z7_IFACE_CONSTR_CODER(i, n) \ + Z7_DECL_IFACE_7ZIP(i, 4, n) \ + { Z7_IFACE_COM7_PURE(i) }; + +#define Z7_IFACEM_ICompressProgressInfo(x) \ + x(SetRatioInfo(const UInt64 *inSize, const UInt64 *outSize)) +Z7_IFACE_CONSTR_CODER(ICompressProgressInfo, 0x04) + /* + SetRatioInfo() + (inSize) can be NULL, if unknown + (outSize) can be NULL, if unknown + returns: + S_OK + E_ABORT : Break by user + another error codes + */ + +#define Z7_IFACEM_ICompressCoder(x) \ + x(Code(ISequentialInStream *inStream, ISequentialOutStream *outStream, \ + const UInt64 *inSize, const UInt64 *outSize, \ + ICompressProgressInfo *progress)) +Z7_IFACE_CONSTR_CODER(ICompressCoder, 0x05) + +#define Z7_IFACEM_ICompressCoder2(x) \ + x(Code(ISequentialInStream * const *inStreams, const UInt64 *const *inSizes, UInt32 numInStreams, \ + ISequentialOutStream *const *outStreams, const UInt64 *const *outSizes, UInt32 numOutStreams, \ + ICompressProgressInfo *progress)) +Z7_IFACE_CONSTR_CODER(ICompressCoder2, 0x18) + +/* + ICompressCoder::Code + ICompressCoder2::Code + + returns: + S_OK : OK + S_FALSE : data error (for decoders) + E_OUTOFMEMORY : memory allocation error + E_NOTIMPL : unsupported encoding method (for decoders) + another error code : some error. For example, it can be error code received from inStream or outStream function. + + Parameters: + (inStream != NULL) + (outStream != NULL) + + if (inSize != NULL) + { + Encoders in 7-Zip ignore (inSize). + Decoder can use (*inSize) to check that stream was decoded correctly. + Some decoders in 7-Zip check it, if (full_decoding mode was set via ICompressSetFinishMode) + } + + If it's required to limit the reading from input stream (inStream), it can + be done with ISequentialInStream implementation. + + if (outSize != NULL) + { + Encoders in 7-Zip ignore (outSize). + Decoder unpacks no more than (*outSize) bytes. + } + + (progress == NULL) is allowed. + + + Decoding with Code() function + ----------------------------- + + You can request some interfaces before decoding + - ICompressSetDecoderProperties2 + - ICompressSetFinishMode + + If you need to decode full stream: + { + 1) try to set full_decoding mode with ICompressSetFinishMode::SetFinishMode(1); + 2) call the Code() function with specified (inSize) and (outSize), if these sizes are known. + } + + If you need to decode only part of stream: + { + 1) try to set partial_decoding mode with ICompressSetFinishMode::SetFinishMode(0); + 2) Call the Code() function with specified (inSize = NULL) and specified (outSize). + } + + Encoding with Code() function + ----------------------------- + + You can request some interfaces : + - ICompressSetCoderProperties - use it before encoding to set properties + - ICompressWriteCoderProperties - use it before or after encoding to request encoded properties. + + ICompressCoder2 is used when (numInStreams != 1 || numOutStreams != 1) + The rules are similar to ICompressCoder rules +*/ + + +namespace NCoderPropID +{ + enum EEnum + { + kDefaultProp = 0, + kDictionarySize, // VT_UI4 + kUsedMemorySize, // VT_UI4 + kOrder, // VT_UI4 + kBlockSize, // VT_UI4 or VT_UI8 + kPosStateBits, // VT_UI4 + kLitContextBits, // VT_UI4 + kLitPosBits, // VT_UI4 + kNumFastBytes, // VT_UI4 + kMatchFinder, // VT_BSTR + kMatchFinderCycles, // VT_UI4 + kNumPasses, // VT_UI4 + kAlgorithm, // VT_UI4 + kNumThreads, // VT_UI4 + kEndMarker, // VT_BOOL + kLevel, // VT_UI4 + kReduceSize, // VT_UI8 : it's estimated size of largest data stream that will be compressed + // encoder can use this value to reduce dictionary size and allocate data buffers + + kExpectedDataSize, // VT_UI8 : for ICompressSetCoderPropertiesOpt : + // it's estimated size of current data stream + // real data size can differ from that size + // encoder can use this value to optimize encoder initialization + + kBlockSize2, // VT_UI4 or VT_UI8 + kCheckSize, // VT_UI4 : size of digest in bytes + kFilter, // VT_BSTR + kMemUse, // VT_UI8 + kAffinity, // VT_UI8 + kBranchOffset, // VT_UI4 + kHashBits, // VT_UI4 + /* + // kHash3Bits, // VT_UI4 + // kHash2Bits, // VT_UI4 + // kChainBits, // VT_UI4 + kChainSize, // VT_UI4 + kNativeLevel, // VT_UI4 + kFast, // VT_UI4 + kMinMatch, // VT_UI4 The minimum slen is 3 and the maximum is 7. + kOverlapLog, // VT_UI4 The minimum ovlog is 0 and the maximum is 9. (default: 6) + kRowMatchFinder, // VT_BOOL + kLdmEnable, // VT_BOOL + // kLdmWindowSizeLog, // VT_UI4 + kLdmWindowSize, // VT_UI4 + kLdmHashLog, // VT_UI4 The minimum ldmhlog is 6 and the maximum is 26 (default: 20). + kLdmMinMatchLength, // VT_UI4 The minimum ldmslen is 4 and the maximum is 4096 (default: 64). + kLdmBucketSizeLog, // VT_UI4 The minimum ldmblog is 0 and the maximum is 8 (default: 3). + kLdmHashRateLog, // VT_UI4 The default value is wlog - ldmhlog. + kWriteUnpackSizeFlag, // VT_BOOL + kUsePledged, // VT_BOOL + kUseSizeHintPledgedForSmall, // VT_BOOL + kUseSizeHintForEach, // VT_BOOL + kUseSizeHintGlobal, // VT_BOOL + kParamSelectMode, // VT_UI4 + // kSearchLog, // VT_UI4 The minimum slog is 1 and the maximum is 26 + // kTargetLen, // VT_UI4 The minimum tlen is 0 and the maximum is 999. + */ + k_NUM_DEFINED + }; +} + +#define Z7_IFACEM_ICompressSetCoderPropertiesOpt(x) \ + x(SetCoderPropertiesOpt(const PROPID *propIDs, const PROPVARIANT *props, UInt32 numProps)) +Z7_IFACE_CONSTR_CODER(ICompressSetCoderPropertiesOpt, 0x1F) + + +#define Z7_IFACEM_ICompressSetCoderProperties(x) \ + x(SetCoderProperties(const PROPID *propIDs, const PROPVARIANT *props, UInt32 numProps)) +Z7_IFACE_CONSTR_CODER(ICompressSetCoderProperties, 0x20) + +/* +#define Z7_IFACEM_ICompressSetDecoderProperties(x) \ + x(SetDecoderProperties(ISequentialInStream *inStream)) +Z7_IFACE_CONSTR_CODER(ICompressSetDecoderProperties, 0x21) +*/ + +#define Z7_IFACEM_ICompressSetDecoderProperties2(x) \ + x(SetDecoderProperties2(const Byte *data, UInt32 size)) +Z7_IFACE_CONSTR_CODER(ICompressSetDecoderProperties2, 0x22) + /* returns: + S_OK + E_NOTIMP : unsupported properties + E_INVALIDARG : incorrect (or unsupported) properties + E_OUTOFMEMORY : memory allocation error + */ + + +#define Z7_IFACEM_ICompressWriteCoderProperties(x) \ + x(WriteCoderProperties(ISequentialOutStream *outStream)) +Z7_IFACE_CONSTR_CODER(ICompressWriteCoderProperties, 0x23) + +#define Z7_IFACEM_ICompressGetInStreamProcessedSize(x) \ + x(GetInStreamProcessedSize(UInt64 *value)) +Z7_IFACE_CONSTR_CODER(ICompressGetInStreamProcessedSize, 0x24) + +#define Z7_IFACEM_ICompressSetCoderMt(x) \ + x(SetNumberOfThreads(UInt32 numThreads)) +Z7_IFACE_CONSTR_CODER(ICompressSetCoderMt, 0x25) + +#define Z7_IFACEM_ICompressSetFinishMode(x) \ + x(SetFinishMode(UInt32 finishMode)) +Z7_IFACE_CONSTR_CODER(ICompressSetFinishMode, 0x26) + /* finishMode: + 0 : partial decoding is allowed. It's default mode for ICompressCoder::Code(), if (outSize) is defined. + 1 : full decoding. The stream must be finished at the end of decoding. */ + +#define Z7_IFACEM_ICompressGetInStreamProcessedSize2(x) \ + x(GetInStreamProcessedSize2(UInt32 streamIndex, UInt64 *value)) +Z7_IFACE_CONSTR_CODER(ICompressGetInStreamProcessedSize2, 0x27) + +#define Z7_IFACEM_ICompressSetMemLimit(x) \ + x(SetMemLimit(UInt64 memUsage)) +Z7_IFACE_CONSTR_CODER(ICompressSetMemLimit, 0x28) + + +/* + ICompressReadUnusedFromInBuf is supported by ICoder object + call ReadUnusedFromInBuf() after ICoder::Code(inStream, ...). + ICoder::Code(inStream, ...) decodes data, and the ICoder object is allowed + to read from inStream to internal buffers more data than minimal data required for decoding. + So we can call ReadUnusedFromInBuf() from same ICoder object to read unused input + data from the internal buffer. + in ReadUnusedFromInBuf(): the Coder is not allowed to use (ISequentialInStream *inStream) object, that was sent to ICoder::Code(). +*/ +#define Z7_IFACEM_ICompressReadUnusedFromInBuf(x) \ + x(ReadUnusedFromInBuf(void *data, UInt32 size, UInt32 *processedSize)) +Z7_IFACE_CONSTR_CODER(ICompressReadUnusedFromInBuf, 0x29) + + +#define Z7_IFACEM_ICompressGetSubStreamSize(x) \ + x(GetSubStreamSize(UInt64 subStream, UInt64 *value)) +Z7_IFACE_CONSTR_CODER(ICompressGetSubStreamSize, 0x30) + /* returns: + S_OK : (*value) contains the size or estimated size (can be incorrect size) + S_FALSE : size is undefined + E_NOTIMP : the feature is not implemented + Let's (read_size) is size of data that was already read by ISequentialInStream::Read(). + The caller should call GetSubStreamSize() after each Read() and check sizes: + if (start_of_subStream + *value < read_size) + { + // (*value) is correct, and it's allowed to call GetSubStreamSize() for next subStream: + start_of_subStream += *value; + subStream++; + } + */ + +#define Z7_IFACEM_ICompressSetInStream(x) \ + x(SetInStream(ISequentialInStream *inStream)) \ + x(ReleaseInStream()) +Z7_IFACE_CONSTR_CODER(ICompressSetInStream, 0x31) + +#define Z7_IFACEM_ICompressSetOutStream(x) \ + x(SetOutStream(ISequentialOutStream *outStream)) \ + x(ReleaseOutStream()) +Z7_IFACE_CONSTR_CODER(ICompressSetOutStream, 0x32) + +/* +#define Z7_IFACEM_ICompressSetInStreamSize(x) \ + x(SetInStreamSize(const UInt64 *inSize)) \ +Z7_IFACE_CONSTR_CODER(ICompressSetInStreamSize, 0x33) +*/ + +#define Z7_IFACEM_ICompressSetOutStreamSize(x) \ + x(SetOutStreamSize(const UInt64 *outSize)) +Z7_IFACE_CONSTR_CODER(ICompressSetOutStreamSize, 0x34) + /* That function initializes decoder structures. + Call this function only for stream version of decoder. + if (outSize == NULL), then output size is unknown + if (outSize != NULL), then the decoder must stop decoding after (*outSize) bytes. */ + +#define Z7_IFACEM_ICompressSetBufSize(x) \ + x(SetInBufSize(UInt32 streamIndex, UInt32 size)) \ + x(SetOutBufSize(UInt32 streamIndex, UInt32 size)) + +Z7_IFACE_CONSTR_CODER(ICompressSetBufSize, 0x35) + +#define Z7_IFACEM_ICompressInitEncoder(x) \ + x(InitEncoder()) +Z7_IFACE_CONSTR_CODER(ICompressInitEncoder, 0x36) + /* That function initializes encoder structures. + Call this function only for stream version of encoder. */ + +#define Z7_IFACEM_ICompressSetInStream2(x) \ + x(SetInStream2(UInt32 streamIndex, ISequentialInStream *inStream)) \ + x(ReleaseInStream2(UInt32 streamIndex)) +Z7_IFACE_CONSTR_CODER(ICompressSetInStream2, 0x37) + +/* +#define Z7_IFACEM_ICompressSetOutStream2(x) \ + x(SetOutStream2(UInt32 streamIndex, ISequentialOutStream *outStream)) + x(ReleaseOutStream2(UInt32 streamIndex)) +Z7_IFACE_CONSTR_CODER(ICompressSetOutStream2, 0x38) + +#define Z7_IFACEM_ICompressSetInStreamSize2(x) \ + x(SetInStreamSize2(UInt32 streamIndex, const UInt64 *inSize)) +Z7_IFACE_CONSTR_CODER(ICompressSetInStreamSize2, 0x39) +*/ + +/* +#define Z7_IFACEM_ICompressInSubStreams(x) \ + x(GetNextInSubStream(UInt64 *streamIndexRes, ISequentialInStream **stream)) +Z7_IFACE_CONSTR_CODER(ICompressInSubStreams, 0x3A) + +#define Z7_IFACEM_ICompressOutSubStreams(x) \ + x(GetNextOutSubStream(UInt64 *streamIndexRes, ISequentialOutStream **stream)) +Z7_IFACE_CONSTR_CODER(ICompressOutSubStreams, 0x3B) +*/ + +/* + ICompressFilter + Filter(Byte *data, UInt32 size) + (size) + converts as most as possible bytes required for fast processing. + Some filters have (smallest_fast_block). + For example, (smallest_fast_block == 16) for AES CBC/CTR filters. + If data stream is not finished, caller must call Filter() for larger block: + where (size >= smallest_fast_block). + if (size >= smallest_fast_block) + { + The filter can leave some bytes at the end of data without conversion: + if there are data alignment reasons or speed reasons. + The caller can read additional data from stream and call Filter() again. + } + If data stream was finished, caller can call Filter() for (size < smallest_fast_block) + + (data) parameter: + Some filters require alignment for any Filter() call: + 1) (stream_offset % alignment_size) == (data % alignment_size) + 2) (alignment_size == 2^N) + where (stream_offset) - is the number of bytes that were already filtered before. + The callers of Filter() are required to meet these requirements. + (alignment_size) can be different: + 16 : for AES filters + 4 or 2 : for some branch convert filters + 1 : for another filters + (alignment_size >= 16) is enough for all current filters of 7-Zip. + But the caller can use larger (alignment_size). + Recommended alignment for (data) of Filter() call is (alignment_size == 64). + Also it's recommended to use aligned value for (size): + (size % alignment_size == 0), + if it's not last call of Filter() for current stream. + + returns: (outSize): + if (outSize == 0) : Filter have not converted anything. + So the caller can stop processing, if data stream was finished. + if (outSize <= size) : Filter have converted outSize bytes + if (outSize > size) : Filter have not converted anything. + and it needs at least outSize bytes to convert one block + (it's for crypto block algorithms). +*/ + +#define Z7_IFACEM_ICompressFilter(x) \ + x(Init()) \ + x##2(UInt32, Filter(Byte *data, UInt32 size)) +Z7_IFACE_CONSTR_CODER(ICompressFilter, 0x40) + + +#define Z7_IFACEM_ICompressCodecsInfo(x) \ + x(GetNumMethods(UInt32 *numMethods)) \ + x(GetProperty(UInt32 index, PROPID propID, PROPVARIANT *value)) \ + x(CreateDecoder(UInt32 index, const GUID *iid, void* *coder)) \ + x(CreateEncoder(UInt32 index, const GUID *iid, void* *coder)) +Z7_IFACE_CONSTR_CODER(ICompressCodecsInfo, 0x60) + +#define Z7_IFACEM_ISetCompressCodecsInfo(x) \ + x(SetCompressCodecsInfo(ICompressCodecsInfo *compressCodecsInfo)) +Z7_IFACE_CONSTR_CODER(ISetCompressCodecsInfo, 0x61) + +#define Z7_IFACEM_ICryptoProperties(x) \ + x(SetKey(const Byte *data, UInt32 size)) \ + x(SetInitVector(const Byte *data, UInt32 size)) +Z7_IFACE_CONSTR_CODER(ICryptoProperties, 0x80) + +/* + x(ResetSalt()) +Z7_IFACE_CONSTR_CODER(ICryptoResetSalt, 0x88) +*/ + +#define Z7_IFACEM_ICryptoResetInitVector(x) \ + x(ResetInitVector()) +Z7_IFACE_CONSTR_CODER(ICryptoResetInitVector, 0x8C) + /* Call ResetInitVector() only for encoding. + Call ResetInitVector() before encoding and before WriteCoderProperties(). + Crypto encoder can create random IV in that function. */ + +#define Z7_IFACEM_ICryptoSetPassword(x) \ + x(CryptoSetPassword(const Byte *data, UInt32 size)) +Z7_IFACE_CONSTR_CODER(ICryptoSetPassword, 0x90) + +#define Z7_IFACEM_ICryptoSetCRC(x) \ + x(CryptoSetCRC(UInt32 crc)) +Z7_IFACE_CONSTR_CODER(ICryptoSetCRC, 0xA0) + + +namespace NMethodPropID +{ + enum EEnum + { + kID, + kName, + kDecoder, + kEncoder, + kPackStreams, + kUnpackStreams, + kDescription, + kDecoderIsAssigned, + kEncoderIsAssigned, + kDigestSize, + kIsFilter + }; +} + +namespace NModuleInterfaceType +{ + /* + virtual destructor in IUnknown: + - no : 7-Zip (Windows) + - no : 7-Zip (Linux) (v23) in default mode + - yes : p7zip + - yes : 7-Zip (Linux) before v23 + - yes : 7-Zip (Linux) (v23), if Z7_USE_VIRTUAL_DESTRUCTOR_IN_IUNKNOWN is defined + */ + const UInt32 k_IUnknown_VirtDestructor_No = 0; + const UInt32 k_IUnknown_VirtDestructor_Yes = 1; + const UInt32 k_IUnknown_VirtDestructor_ThisModule = + #if !defined(_WIN32) && defined(Z7_USE_VIRTUAL_DESTRUCTOR_IN_IUNKNOWN) + k_IUnknown_VirtDestructor_Yes; + #else + k_IUnknown_VirtDestructor_No; + #endif +} + +namespace NModulePropID +{ + enum EEnum + { + kInterfaceType, // VT_UI4 + kVersion // VT_UI4 + }; +} + + +#define Z7_IFACEM_IHasher(x) \ + x##2(void, Init()) \ + x##2(void, Update(const void *data, UInt32 size)) \ + x##2(void, Final(Byte *digest)) \ + x##2(UInt32, GetDigestSize()) +Z7_IFACE_CONSTR_CODER(IHasher, 0xC0) + +#define Z7_IFACEM_IHashers(x) \ + x##2(UInt32, GetNumHashers()) \ + x(GetHasherProp(UInt32 index, PROPID propID, PROPVARIANT *value)) \ + x(CreateHasher(UInt32 index, IHasher **hasher)) +Z7_IFACE_CONSTR_CODER(IHashers, 0xC1) + +extern "C" +{ + typedef HRESULT (WINAPI *Func_GetNumberOfMethods)(UInt32 *numMethods); + typedef HRESULT (WINAPI *Func_GetMethodProperty)(UInt32 index, PROPID propID, PROPVARIANT *value); + typedef HRESULT (WINAPI *Func_CreateDecoder)(UInt32 index, const GUID *iid, void **outObject); + typedef HRESULT (WINAPI *Func_CreateEncoder)(UInt32 index, const GUID *iid, void **outObject); + + typedef HRESULT (WINAPI *Func_GetHashers)(IHashers **hashers); + + typedef HRESULT (WINAPI *Func_SetCodecs)(ICompressCodecsInfo *compressCodecsInfo); + typedef HRESULT (WINAPI *Func_GetModuleProp)(PROPID propID, PROPVARIANT *value); +} + +Z7_PURE_INTERFACES_END +#endif diff --git a/libs/archive/thirdparty/CPP/7zip/IDecl.h b/libs/archive/thirdparty/CPP/7zip/IDecl.h new file mode 100644 index 0000000..4dbf1eb --- /dev/null +++ b/libs/archive/thirdparty/CPP/7zip/IDecl.h @@ -0,0 +1,76 @@ +// IDecl.h + +#ifndef ZIP7_INC_IDECL_H +#define ZIP7_INC_IDECL_H + +#include "../Common/Common0.h" +#include "../Common/MyUnknown.h" + +#define k_7zip_GUID_Data1 0x23170F69 +#define k_7zip_GUID_Data2 0x40C1 + +#define k_7zip_GUID_Data3_Common 0x278A + +#define k_7zip_GUID_Data3_Decoder 0x2790 +#define k_7zip_GUID_Data3_Encoder 0x2791 +#define k_7zip_GUID_Data3_Hasher 0x2792 + +#define Z7_DECL_IFACE_7ZIP_SUB(i, _base, groupId, subId) \ + Z7_DEFINE_GUID(IID_ ## i, \ + k_7zip_GUID_Data1, \ + k_7zip_GUID_Data2, \ + k_7zip_GUID_Data3_Common, \ + 0, 0, 0, (groupId), 0, (subId), 0, 0); \ + struct Z7_DECLSPEC_NOVTABLE i: public _base + +#define Z7_DECL_IFACE_7ZIP(i, groupId, subId) \ + Z7_DECL_IFACE_7ZIP_SUB(i, IUnknown, groupId, subId) + + +#ifdef COM_DECLSPEC_NOTHROW +#define Z7_COMWF_B COM_DECLSPEC_NOTHROW STDMETHODIMP +#define Z7_COMWF_B_(t) COM_DECLSPEC_NOTHROW STDMETHODIMP_(t) +#else +#define Z7_COMWF_B STDMETHODIMP +#define Z7_COMWF_B_(t) STDMETHODIMP_(t) +#endif + +#if defined(_MSC_VER) && !defined(COM_DECLSPEC_NOTHROW) +#define Z7_COM7F_B __declspec(nothrow) STDMETHODIMP +#define Z7_COM7F_B_(t) __declspec(nothrow) STDMETHODIMP_(t) +#else +#define Z7_COM7F_B Z7_COMWF_B +#define Z7_COM7F_B_(t) Z7_COMWF_B_(t) +#endif + +// #define Z7_COM7F_E Z7_noexcept +#define Z7_COM7F_E throw() +#define Z7_COM7F_EO Z7_COM7F_E Z7_override +#define Z7_COM7F_EOF Z7_COM7F_EO Z7_final +#define Z7_COM7F_IMF(f) Z7_COM7F_B f Z7_COM7F_E +#define Z7_COM7F_IMF2(t, f) Z7_COM7F_B_(t) f Z7_COM7F_E + +#define Z7_COM7F_PURE(f) virtual Z7_COM7F_IMF(f) =0; +#define Z7_COM7F_PURE2(t, f) virtual Z7_COM7F_IMF2(t, f) =0; +#define Z7_COM7F_IMP(f) Z7_COM7F_IMF(f) Z7_override Z7_final; +#define Z7_COM7F_IMP2(t, f) Z7_COM7F_IMF2(t, f) Z7_override Z7_final; +#define Z7_COM7F_IMP_NONFINAL(f) Z7_COM7F_IMF(f) Z7_override; +#define Z7_COM7F_IMP_NONFINAL2(t, f) Z7_COM7F_IMF2(t, f) Z7_override; + +#define Z7_IFACE_PURE(name) Z7_IFACEN_ ## name(=0;) +#define Z7_IFACE_IMP(name) Z7_IFACEN_ ## name(Z7_override Z7_final;) + +#define Z7_IFACE_COM7_PURE(name) Z7_IFACEM_ ## name(Z7_COM7F_PURE) +#define Z7_IFACE_COM7_IMP(name) Z7_IFACEM_ ## name(Z7_COM7F_IMP) +#define Z7_IFACE_COM7_IMP_NONFINAL(name) Z7_IFACEM_ ## name(Z7_COM7F_IMP_NONFINAL) + + +#define Z7_IFACE_DECL_PURE(name) \ + DECLARE_INTERFACE(name) \ + { Z7_IFACE_PURE(name) }; + +#define Z7_IFACE_DECL_PURE_(name, baseiface) \ + DECLARE_INTERFACE_(name, baseiface) \ + { Z7_IFACE_PURE(name) }; + +#endif diff --git a/libs/archive/thirdparty/CPP/7zip/IPassword.h b/libs/archive/thirdparty/CPP/7zip/IPassword.h new file mode 100644 index 0000000..689f08c --- /dev/null +++ b/libs/archive/thirdparty/CPP/7zip/IPassword.h @@ -0,0 +1,54 @@ +// IPassword.h + +#ifndef ZIP7_INC_IPASSWORD_H +#define ZIP7_INC_IPASSWORD_H + +#include "../Common/MyTypes.h" + +#include "IDecl.h" + +Z7_PURE_INTERFACES_BEGIN + +#define Z7_IFACE_CONSTR_PASSWORD(i, n) \ + Z7_DECL_IFACE_7ZIP(i, 5, n) \ + { Z7_IFACE_COM7_PURE(i) }; + +/* +How to use output parameter (BSTR *password): + +in: The caller is required to set BSTR value as NULL (no string). + The callee (in 7-Zip code) ignores the input value stored in BSTR variable, + +out: The callee rewrites BSTR variable (*password) with new allocated string pointer. + The caller must free BSTR string with function SysFreeString(); +*/ + +#define Z7_IFACEM_ICryptoGetTextPassword(x) \ + x(CryptoGetTextPassword(BSTR *password)) +Z7_IFACE_CONSTR_PASSWORD(ICryptoGetTextPassword, 0x10) + + +/* +CryptoGetTextPassword2() +in: + The caller is required to set BSTR value as NULL (no string). + The caller is not required to set (*passwordIsDefined) value. + +out: + Return code: != S_OK : error code + Return code: S_OK : success + + if (*passwordIsDefined == 1), the variable (*password) contains password string + + if (*passwordIsDefined == 0), the password is not defined, + but the callee still could set (*password) to some allocated string, for example, as empty string. + + The caller must free BSTR string with function SysFreeString() +*/ + +#define Z7_IFACEM_ICryptoGetTextPassword2(x) \ + x(CryptoGetTextPassword2(Int32 *passwordIsDefined, BSTR *password)) +Z7_IFACE_CONSTR_PASSWORD(ICryptoGetTextPassword2, 0x11) + +Z7_PURE_INTERFACES_END +#endif diff --git a/libs/archive/thirdparty/CPP/7zip/IProgress.h b/libs/archive/thirdparty/CPP/7zip/IProgress.h new file mode 100644 index 0000000..6714983 --- /dev/null +++ b/libs/archive/thirdparty/CPP/7zip/IProgress.h @@ -0,0 +1,20 @@ +// IProgress.h + +#ifndef ZIP7_INC_IPROGRESS_H +#define ZIP7_INC_IPROGRESS_H + +#include "../Common/MyTypes.h" + +#include "IDecl.h" + +Z7_PURE_INTERFACES_BEGIN + +#define Z7_IFACEM_IProgress(x) \ + x(SetTotal(UInt64 total)) \ + x(SetCompleted(const UInt64 *completeValue)) \ + +Z7_DECL_IFACE_7ZIP(IProgress, 0, 5) + { Z7_IFACE_COM7_PURE(IProgress) }; + +Z7_PURE_INTERFACES_END +#endif diff --git a/libs/archive/thirdparty/CPP/7zip/IStream.h b/libs/archive/thirdparty/CPP/7zip/IStream.h new file mode 100644 index 0000000..0c44a91 --- /dev/null +++ b/libs/archive/thirdparty/CPP/7zip/IStream.h @@ -0,0 +1,210 @@ +// IStream.h + +#ifndef ZIP7_INC_ISTREAM_H +#define ZIP7_INC_ISTREAM_H + +#include "../Common/Common0.h" +#include "../Common/MyTypes.h" +#include "../Common/MyWindows.h" + +#include "IDecl.h" + +Z7_PURE_INTERFACES_BEGIN + +#define Z7_IFACE_CONSTR_STREAM_SUB(i, base, n) \ + Z7_DECL_IFACE_7ZIP_SUB(i, base, 3, n) \ + { Z7_IFACE_COM7_PURE(i) }; + +#define Z7_IFACE_CONSTR_STREAM(i, n) \ + Z7_IFACE_CONSTR_STREAM_SUB(i, IUnknown, n) + + +/* +ISequentialInStream::Read() + The requirement for caller: (processedSize != NULL). + The callee can allow (processedSize == NULL) for compatibility reasons. + + if (size == 0), this function returns S_OK and (*processedSize) is set to 0. + + if (size != 0) + { + Partial read is allowed: (*processedSize <= avail_size && *processedSize <= size), + where (avail_size) is the size of remaining bytes in stream. + If (avail_size != 0), this function must read at least 1 byte: (*processedSize > 0). + You must call Read() in loop, if you need to read exact amount of data. + } + + If seek pointer before Read() call was changed to position past the end of stream: + if (seek_pointer >= stream_size), this function returns S_OK and (*processedSize) is set to 0. + + ERROR CASES: + If the function returns error code, then (*processedSize) is size of + data written to (data) buffer (it can be data before error or data with errors). + The recommended way for callee to work with reading errors: + 1) write part of data before error to (data) buffer and return S_OK. + 2) return error code for further calls of Read(). +*/ +#define Z7_IFACEM_ISequentialInStream(x) \ + x(Read(void *data, UInt32 size, UInt32 *processedSize)) +Z7_IFACE_CONSTR_STREAM(ISequentialInStream, 0x01) + + +/* +ISequentialOutStream::Write() + The requirement for caller: (processedSize != NULL). + The callee can allow (processedSize == NULL) for compatibility reasons. + + if (size != 0) + { + Partial write is allowed: (*processedSize <= size), + but this function must write at least 1 byte: (*processedSize > 0). + You must call Write() in loop, if you need to write exact amount of data. + } + + ERROR CASES: + If the function returns error code, then (*processedSize) is size of + data written from (data) buffer. +*/ +#define Z7_IFACEM_ISequentialOutStream(x) \ + x(Write(const void *data, UInt32 size, UInt32 *processedSize)) +Z7_IFACE_CONSTR_STREAM(ISequentialOutStream, 0x02) + + +#ifdef _WIN32 + +#ifdef __HRESULT_FROM_WIN32 +#define HRESULT_WIN32_ERROR_NEGATIVE_SEEK __HRESULT_FROM_WIN32(ERROR_NEGATIVE_SEEK) +#else +#define HRESULT_WIN32_ERROR_NEGATIVE_SEEK HRESULT_FROM_WIN32(ERROR_NEGATIVE_SEEK) +#endif + +#else + +#define HRESULT_WIN32_ERROR_NEGATIVE_SEEK MY_E_ERROR_NEGATIVE_SEEK + +#endif + + +/* +IInStream::Seek() / IOutStream::Seek() + If you seek to position before the beginning of the stream, + Seek() function returns error code: + Recommended error code is __HRESULT_FROM_WIN32(ERROR_NEGATIVE_SEEK). + or STG_E_INVALIDFUNCTION + It is allowed to seek past the end of the stream. + if Seek() returns error, then the value of *newPosition is undefined. +*/ + +#define Z7_IFACEM_IInStream(x) \ + x(Seek(Int64 offset, UInt32 seekOrigin, UInt64 *newPosition)) +Z7_IFACE_CONSTR_STREAM_SUB(IInStream, ISequentialInStream, 0x03) + +#define Z7_IFACEM_IOutStream(x) \ + x(Seek(Int64 offset, UInt32 seekOrigin, UInt64 *newPosition)) \ + x(SetSize(UInt64 newSize)) +Z7_IFACE_CONSTR_STREAM_SUB(IOutStream, ISequentialOutStream, 0x04) + +#define Z7_IFACEM_IStreamGetSize(x) \ + x(GetSize(UInt64 *size)) +Z7_IFACE_CONSTR_STREAM(IStreamGetSize, 0x06) + +#define Z7_IFACEM_IOutStreamFinish(x) \ + x(OutStreamFinish()) +Z7_IFACE_CONSTR_STREAM(IOutStreamFinish, 0x07) + +#define Z7_IFACEM_IStreamGetProps(x) \ + x(GetProps(UInt64 *size, FILETIME *cTime, FILETIME *aTime, FILETIME *mTime, UInt32 *attrib)) +Z7_IFACE_CONSTR_STREAM(IStreamGetProps, 0x08) + + +struct CStreamFileProps +{ + UInt64 Size; + UInt64 VolID; + UInt64 FileID_Low; + UInt64 FileID_High; + UInt32 NumLinks; + UInt32 Attrib; + FILETIME CTime; + FILETIME ATime; + FILETIME MTime; +}; + + +#define Z7_IFACEM_IStreamGetProps2(x) \ + x(GetProps2(CStreamFileProps *props)) +Z7_IFACE_CONSTR_STREAM(IStreamGetProps2, 0x09) + +#define Z7_IFACEM_IStreamGetProp(x) \ + x(GetProperty(PROPID propID, PROPVARIANT *value)) \ + x(ReloadProps()) +Z7_IFACE_CONSTR_STREAM(IStreamGetProp, 0x0a) + + +/* +IStreamSetRestriction::SetRestriction(UInt64 begin, UInt64 end) + + It sets region of data in output stream that is restricted. + For restricted region it's expected (or allowed) + that the caller can write to same region with different calls of Write()/SetSize(). + Another regions of output stream will be supposed as non-restricted: + - The callee usually doesn't flush the data in restricted region. + - The callee usually can flush data from non-restricted region after writing. + +Actual restiction rules depend also from current stream position. +It's recommended to call SetRestriction() just before the Write() call. +So the callee can optimize writing and flushing, if that Write() +operation is not restricted. + +Note: Each new call of SetRestriction() sets new restictions, +so previous restrction calls has no effect anymore. + +inputs: + + (begin > end) is not allowed, and returns E_FAIL; + + if (begin == end) + { + No restriction. + The caller will call Write() in sequential order. + After SetRestriction(begin, begin), but before next call of SetRestriction() + { + Additional condition: + it's expected that current stream seek position is equal to stream size. + The callee can make final flushing for any data before current stream seek position. + For each Write(size) call: + The callee can make final flushing for that new written data. + } + The pair of values (begin == 0 && end == 0) is recommended to remove write restriction. + } + + if (begin < end) + { + it means that callee must NOT flush any data in region [begin, end). + The caller is allowed to Seek() to that region and rewrite the + data in that restriction region. + if (end == (UInt64)(Int64)-1) + { + there is no upper bound for restricted region. + So non-restricted region will be [0, begin) in that case + } + } + + returns: + - if (begin > end) it return ERROR code (E_FAIL) + - S_OK : if no errors. + - Also the call of SetRestriction() can initiate the flushing of already written data. + So it can return the result of that flushing. + + Note: IOutStream::SetSize() also can change the data. + So it's not expected the call + IOutStream::SetSize() to region that was written before as unrestricted. +*/ + +#define Z7_IFACEM_IStreamSetRestriction(x) \ + x(SetRestriction(UInt64 begin, UInt64 end)) \ + +Z7_IFACE_CONSTR_STREAM(IStreamSetRestriction, 0x10) + +Z7_PURE_INTERFACES_END +#endif diff --git a/libs/archive/thirdparty/CPP/7zip/MyVersion.h b/libs/archive/thirdparty/CPP/7zip/MyVersion.h new file mode 100644 index 0000000..8f52a12 --- /dev/null +++ b/libs/archive/thirdparty/CPP/7zip/MyVersion.h @@ -0,0 +1,2 @@ +#define USE_COPYRIGHT_CR +#include "../../C/7zVersion.h" diff --git a/libs/archive/thirdparty/CPP/7zip/PropID.h b/libs/archive/thirdparty/CPP/7zip/PropID.h new file mode 100644 index 0000000..e074794 --- /dev/null +++ b/libs/archive/thirdparty/CPP/7zip/PropID.h @@ -0,0 +1,178 @@ +// PropID.h + +#ifndef ZIP7_INC_7ZIP_PROP_ID_H +#define ZIP7_INC_7ZIP_PROP_ID_H + +#include "../Common/MyTypes.h" + +enum +{ + kpidNoProperty = 0, + kpidMainSubfile, + kpidHandlerItemIndex, + kpidPath, + kpidName, + kpidExtension, + kpidIsDir, + kpidSize, + kpidPackSize, + kpidAttrib, + kpidCTime, + kpidATime, + kpidMTime, + kpidSolid, + kpidCommented, + kpidEncrypted, + kpidSplitBefore, + kpidSplitAfter, + kpidDictionarySize, + kpidCRC, + kpidType, + kpidIsAnti, + kpidMethod, + kpidHostOS, + kpidFileSystem, + kpidUser, + kpidGroup, + kpidBlock, + kpidComment, + kpidPosition, + kpidPrefix, + kpidNumSubDirs, + kpidNumSubFiles, + kpidUnpackVer, + kpidVolume, + kpidIsVolume, + kpidOffset, + kpidLinks, + kpidNumBlocks, + kpidNumVolumes, + kpidTimeType, + kpidBit64, + kpidBigEndian, + kpidCpu, + kpidPhySize, + kpidHeadersSize, + kpidChecksum, + kpidCharacts, + kpidVa, + kpidId, + kpidShortName, + kpidCreatorApp, + kpidSectorSize, + kpidPosixAttrib, + kpidSymLink, + kpidError, + kpidTotalSize, + kpidFreeSpace, + kpidClusterSize, + kpidVolumeName, + kpidLocalName, + kpidProvider, + kpidNtSecure, + kpidIsAltStream, + kpidIsAux, + kpidIsDeleted, + kpidIsTree, + kpidSha1, + kpidSha256, + kpidErrorType, + kpidNumErrors, + kpidErrorFlags, + kpidWarningFlags, + kpidWarning, + kpidNumStreams, + kpidNumAltStreams, + kpidAltStreamsSize, + kpidVirtualSize, + kpidUnpackSize, + kpidTotalPhySize, + kpidVolumeIndex, + kpidSubType, + kpidShortComment, + kpidCodePage, + kpidIsNotArcType, + kpidPhySizeCantBeDetected, + kpidZerosTailIsAllowed, + kpidTailSize, + kpidEmbeddedStubSize, + kpidNtReparse, + kpidHardLink, + kpidINode, + kpidStreamId, + kpidReadOnly, + kpidOutName, + kpidCopyLink, + kpidArcFileName, + kpidIsHash, + kpidChangeTime, + kpidUserId, + kpidGroupId, + kpidDeviceMajor, + kpidDeviceMinor, + kpidDevMajor, + kpidDevMinor, + + kpid_NUM_DEFINED, + + kpidUserDefined = 0x10000 +}; + +extern const Byte k7z_PROPID_To_VARTYPE[kpid_NUM_DEFINED]; // VARTYPE + +const UInt32 kpv_ErrorFlags_IsNotArc = 1 << 0; +const UInt32 kpv_ErrorFlags_HeadersError = 1 << 1; +const UInt32 kpv_ErrorFlags_EncryptedHeadersError = 1 << 2; +const UInt32 kpv_ErrorFlags_UnavailableStart = 1 << 3; +const UInt32 kpv_ErrorFlags_UnconfirmedStart = 1 << 4; +const UInt32 kpv_ErrorFlags_UnexpectedEnd = 1 << 5; +const UInt32 kpv_ErrorFlags_DataAfterEnd = 1 << 6; +const UInt32 kpv_ErrorFlags_UnsupportedMethod = 1 << 7; +const UInt32 kpv_ErrorFlags_UnsupportedFeature = 1 << 8; +const UInt32 kpv_ErrorFlags_DataError = 1 << 9; +const UInt32 kpv_ErrorFlags_CrcError = 1 << 10; +// const UInt32 kpv_ErrorFlags_Unsupported = 1 << 11; + +/* +linux ctime : + file metadata was last changed. + changing the file modification time + counts as a metadata change, so will also have the side effect of updating the ctime. + +PROPVARIANT for timestamps in 7-Zip: +{ + vt = VT_FILETIME + wReserved1: set precision level + 0 : base value (backward compatibility value) + only filetime is used (7 digits precision). + wReserved2 and wReserved3 can contain random data + 1 : Unix (1 sec) + 2 : DOS (2 sec) + 3 : High Precision (1 ns) + 16 - 3 : (reserved) = 1 day + 16 - 2 : (reserved) = 1 hour + 16 - 1 : (reserved) = 1 minute + 16 + 0 : 1 sec (0 digits after point) + 16 + (1,2,3,4,5,6,7,8,9) : set subsecond precision level : + (number of decimal digits after point) + 16 + 9 : 1 ns (9 digits after point) + wReserved2 = ns % 100 : if (8 or 9 digits pecision) + = 0 : if not (8 or 9 digits pecision) + wReserved3 = 0; + filetime +} + +NOTE: TAR-PAX archives created by GNU TAR don't keep + whole information about original level of precision, + and timestamp are stored in reduced form, where tail zero + digits after point are removed. + So 7-Zip can return different precision levels for different items for such TAR archives. +*/ + +/* +TimePrec returned by IOutArchive::GetFileTimeType() +is used only for updating, when we compare MTime timestamp +from archive with timestamp from directory. +*/ + +#endif diff --git a/libs/archive/thirdparty/CPP/Common/Common.h b/libs/archive/thirdparty/CPP/Common/Common.h new file mode 100644 index 0000000..cde0c38 --- /dev/null +++ b/libs/archive/thirdparty/CPP/Common/Common.h @@ -0,0 +1,28 @@ +// Common.h + +#if defined(_MSC_VER) && _MSC_VER >= 1800 +#pragma warning(disable : 4464) // relative include path contains '..' +#endif + +#ifndef ZIP7_INC_COMMON_H +#define ZIP7_INC_COMMON_H + +#include "../../C/Precomp.h" +#include "Common0.h" +#include "MyWindows.h" + +/* +This file is included to all cpp files in 7-Zip. +Each folder contains StdAfx.h file that includes "Common.h". +So 7-Zip includes "Common.h" in both modes: + with precompiled StdAfx.h +and + without precompiled StdAfx.h + +include "Common.h" before other h files of 7-zip, + if you need predefined macros. +do not include "Common.h", if you need only interfaces, + and you don't need predefined macros. +*/ + +#endif diff --git a/libs/archive/thirdparty/CPP/Common/Common0.h b/libs/archive/thirdparty/CPP/Common/Common0.h new file mode 100644 index 0000000..55606cd --- /dev/null +++ b/libs/archive/thirdparty/CPP/Common/Common0.h @@ -0,0 +1,330 @@ +// Common0.h + +#if defined(_MSC_VER) && _MSC_VER >= 1800 +#pragma warning(disable : 4464) // relative include path contains '..' +#endif + +#ifndef ZIP7_INC_COMMON0_H +#define ZIP7_INC_COMMON0_H + +#include "../../C/Compiler.h" + +/* +This file contains compiler related things for cpp files. +This file is included to all cpp files in 7-Zip via "Common.h". +Also this file is included in "IDecl.h" (that is included in interface files). +So external modules can use 7-Zip interfaces without +predefined macros defined in "Common.h". +*/ + +#ifdef _MSC_VER + #pragma warning(disable : 4710) // function not inlined + // 'CUncopyable::CUncopyable': + #pragma warning(disable : 4514) // unreferenced inline function has been removed + #if _MSC_VER < 1300 + #pragma warning(disable : 4702) // unreachable code + #pragma warning(disable : 4714) // function marked as __forceinline not inlined + #pragma warning(disable : 4786) // identifier was truncated to '255' characters in the debug information + #endif + #if _MSC_VER < 1400 + #pragma warning(disable : 4511) // copy constructor could not be generated // #pragma warning(disable : 4512) // assignment operator could not be generated + #pragma warning(disable : 4512) // assignment operator could not be generated + #endif + #if _MSC_VER > 1400 && _MSC_VER <= 1900 + // #pragma warning(disable : 4996) + // strcat: This function or variable may be unsafe + // GetVersion was declared deprecated + #endif + +#if _MSC_VER > 1200 +// -Wall warnings + +#if _MSC_VER <= 1600 +#pragma warning(disable : 4917) // 'OLE_HANDLE' : a GUID can only be associated with a class, interface or namespace +#endif + +// #pragma warning(disable : 4061) // enumerator '' in switch of enum '' is not explicitly handled by a case label +// #pragma warning(disable : 4266) // no override available for virtual member function from base ''; function is hidden +#pragma warning(disable : 4625) // copy constructor was implicitly defined as deleted +#pragma warning(disable : 4626) // assignment operator was implicitly defined as deleted +#if _MSC_VER >= 1600 && _MSC_VER < 1920 +#pragma warning(disable : 4571) // Informational: catch(...) semantics changed since Visual C++ 7.1; structured exceptions (SEH) are no longer caught +#endif +#if _MSC_VER >= 1600 +#pragma warning(disable : 4365) // 'initializing' : conversion from 'int' to 'unsigned int', signed / unsigned mismatch +#endif +#if _MSC_VER < 1800 +// we disable the warning, if we don't use 'final' in class +#pragma warning(disable : 4265) // class has virtual functions, but destructor is not virtual +#endif + +#if _MSC_VER >= 1900 +#pragma warning(disable : 5026) // move constructor was implicitly defined as deleted +#pragma warning(disable : 5027) // move assignment operator was implicitly defined as deleted +#endif +#if _MSC_VER >= 1912 +#pragma warning(disable : 5039) // pointer or reference to potentially throwing function passed to 'extern "C"' function under - EHc.Undefined behavior may occur if this function throws an exception. +#endif +#if _MSC_VER >= 1925 +// #pragma warning(disable : 5204) // 'ISequentialInStream' : class has virtual functions, but its trivial destructor is not virtual; instances of objects derived from this class may not be destructed correctly +#endif +#if _MSC_VER >= 1934 +// #pragma warning(disable : 5264) // const variable is not used +#endif + +#endif // _MSC_VER > 1200 +#endif // _MSC_VER + + +#if defined(_MSC_VER) // && !defined(__clang__) +#define Z7_DECLSPEC_NOTHROW __declspec(nothrow) +#elif defined(__clang__) || defined(__GNUC__) +#define Z7_DECLSPEC_NOTHROW __attribute__((nothrow)) +#else +#define Z7_DECLSPEC_NOTHROW +#endif + +/* +#if defined (_MSC_VER) && _MSC_VER >= 1900 \ + || defined(__clang__) && __clang_major__ >= 6 \ + || defined(__GNUC__) && __GNUC__ >= 6 + #define Z7_noexcept noexcept +#else + #define Z7_noexcept throw() +#endif +*/ + + +#if defined(__clang__) + +#if /* defined(_WIN32) && */ __clang_major__ >= 16 +#pragma GCC diagnostic ignored "-Wc++98-compat-pedantic" +#endif + +#if __clang_major__ >= 4 && __clang_major__ < 12 && !defined(_WIN32) +/* +if compiled with new GCC libstdc++, GCC libstdc++ can use: +13.2.0/include/c++/ + : #define _NEW + : #define _GLIBCXX_STDLIB_H 1 +*/ +#pragma GCC diagnostic ignored "-Wreserved-id-macro" +#endif + +// noexcept, final, = delete +#pragma GCC diagnostic ignored "-Wc++98-compat" +#if __clang_major__ >= 4 +// throw() dynamic exception specifications are deprecated +#pragma GCC diagnostic ignored "-Wdeprecated-dynamic-exception-spec" +#endif + +#if __clang_major__ <= 6 // check it +#pragma GCC diagnostic ignored "-Wsign-conversion" +#endif + +#pragma GCC diagnostic ignored "-Wold-style-cast" +#pragma GCC diagnostic ignored "-Wglobal-constructors" +#pragma GCC diagnostic ignored "-Wexit-time-destructors" + +#if defined(Z7_LLVM_CLANG_VERSION) && __clang_major__ >= 18 // 18.1.0RC +#pragma GCC diagnostic ignored "-Wswitch-default" +#endif +// #pragma GCC diagnostic ignored "-Wunused-private-field" +// #pragma GCC diagnostic ignored "-Wnonportable-system-include-path" +// #pragma GCC diagnostic ignored "-Wsuggest-override" +// #pragma GCC diagnostic ignored "-Wsign-conversion" +// #pragma GCC diagnostic ignored "-Winconsistent-missing-override" +// #pragma GCC diagnostic ignored "-Wsuggest-destructor-override" +// #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" +// #pragma GCC diagnostic ignored "-Wdeprecated-copy-with-user-provided-dtor" +// #pragma GCC diagnostic ignored "-Wdeprecated-copy-dtor" +// #ifndef _WIN32 +// #pragma GCC diagnostic ignored "-Wweak-vtables" +// #endif +/* +#if defined(Z7_GCC_VERSION) && (Z7_GCC_VERSION >= 40400) \ + || defined(Z7_CLANG_VERSION) && (Z7_CLANG_VERSION >= 30000) +// enumeration values not explicitly handled in switch +#pragma GCC diagnostic ignored "-Wswitch-enum" +#endif +*/ +#endif // __clang__ + + +#ifdef __GNUC__ +// #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +#endif + + +/* There is BUG in MSVC 6.0 compiler for operator new[]: + It doesn't check overflow, when it calculates size in bytes for allocated array. + So we can use Z7_ARRAY_NEW macro instead of new[] operator. */ + +#if defined(_MSC_VER) && (_MSC_VER == 1200) && !defined(_WIN64) + #define Z7_ARRAY_NEW(p, T, size) p = new T[((size) > 0xFFFFFFFFu / sizeof(T)) ? 0xFFFFFFFFu / sizeof(T) : (size)]; +#else + #define Z7_ARRAY_NEW(p, T, size) p = new T[size]; +#endif + +#if (defined(__GNUC__) && (__GNUC__ >= 8)) + #define Z7_ATTR_NORETURN __attribute__((noreturn)) +#elif (defined(__clang__) && (__clang_major__ >= 3)) + #if __has_feature(cxx_attributes) + #define Z7_ATTR_NORETURN [[noreturn]] + #else + #define Z7_ATTR_NORETURN __attribute__((noreturn)) + #endif +#elif (defined(_MSC_VER) && (_MSC_VER >= 1900)) + #define Z7_ATTR_NORETURN [[noreturn]] +#else + #define Z7_ATTR_NORETURN +#endif + + +// final in "GCC 4.7.0" +// In C++98 and C++03 code the alternative spelling __final can be used instead (this is a GCC extension.) + +#if defined (__cplusplus) && __cplusplus >= 201103L \ + || defined(_MSC_VER) && _MSC_VER >= 1800 \ + || defined(__clang__) && __clang_major__ >= 4 \ + /* || defined(__GNUC__) && __GNUC__ >= 9 */ + #define Z7_final final + #if defined(__clang__) && __cplusplus < 201103L + #pragma GCC diagnostic ignored "-Wc++11-extensions" + #endif +#elif defined (__cplusplus) && __cplusplus >= 199711L \ + && defined(__GNUC__) && __GNUC__ >= 4 && !defined(__clang__) + #define Z7_final __final +#else + #define Z7_final + #if defined(__clang__) && __clang_major__ >= 4 \ + || defined(__GNUC__) && __GNUC__ >= 4 + #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" + #endif +#endif + +#define Z7_class_final(c) class c Z7_final + + +#if defined (__cplusplus) && __cplusplus >= 201103L \ + || (defined(_MSC_VER) && _MSC_VER >= 1800) + #define Z7_CPP_IS_SUPPORTED_default + #define Z7_eq_delete = delete + // #define Z7_DECL_DEFAULT_COPY_CONSTRUCTOR_IF_SUPPORTED(c) c(const c& k) = default; +#else + #define Z7_eq_delete + // #define Z7_DECL_DEFAULT_COPY_CONSTRUCTOR_IF_SUPPORTED(c) +#endif + + +#if defined(__cplusplus) && (__cplusplus >= 201103L) \ + || defined(_MSC_VER) && (_MSC_VER >= 1400) /* && (_MSC_VER != 1600) */ \ + || defined(__clang__) && __clang_major__ >= 4 + #if defined(_MSC_VER) && (_MSC_VER == 1600) /* && (_MSC_VER != 1600) */ + #pragma warning(disable : 4481) // nonstandard extension used: override specifier 'override' + #define Z7_DESTRUCTOR_override + #else + #define Z7_DESTRUCTOR_override override + #endif + #define Z7_override override +#else + #define Z7_override + #define Z7_DESTRUCTOR_override +#endif + + + +#define Z7_CLASS_NO_COPY(cls) \ + private: \ + cls(const cls &) Z7_eq_delete; \ + cls &operator=(const cls &) Z7_eq_delete; + +class CUncopyable +{ +protected: + CUncopyable() {} // allow constructor + // ~CUncopyable() {} + Z7_CLASS_NO_COPY(CUncopyable) +}; + +#define MY_UNCOPYABLE :private CUncopyable +// #define MY_UNCOPYABLE + + +// typedef void (*Z7_void_Function)(void); + +#if defined(__clang__) || defined(__GNUC__) +#define Z7_CAST_FUNC(t, e) reinterpret_cast(reinterpret_cast(e)) +#else +#define Z7_CAST_FUNC(t, e) reinterpret_cast(reinterpret_cast(e)) +// #define Z7_CAST_FUNC(t, e) reinterpret_cast(e) +#endif + +#define Z7_GET_PROC_ADDRESS(func_type, hmodule, func_name) \ + Z7_CAST_FUNC(func_type, GetProcAddress(hmodule, func_name)) + +// || defined(__clang__) +// || defined(__GNUC__) + +#if defined(_MSC_VER) && (_MSC_VER >= 1400) +#define Z7_DECLSPEC_NOVTABLE __declspec(novtable) +#else +#define Z7_DECLSPEC_NOVTABLE +#endif + +#ifdef __clang__ +#define Z7_PURE_INTERFACES_BEGIN \ +_Pragma("GCC diagnostic push") \ +_Pragma("GCC diagnostic ignored \"-Wnon-virtual-dtor\"") +_Pragma("GCC diagnostic ignored \"-Wweak-vtables\"") +#define Z7_PURE_INTERFACES_END \ +_Pragma("GCC diagnostic pop") +#else +#define Z7_PURE_INTERFACES_BEGIN +#define Z7_PURE_INTERFACES_END +#endif + +// NewHandler.h and NewHandler.cpp redefine operator new() to throw exceptions, if compiled with old MSVC compilers +#include "NewHandler.h" + +/* +// #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#ifndef ARRAY_SIZE +#define ARRAY_SIZE(a) Z7_ARRAY_SIZE(a) +#endif +*/ + +#endif // ZIP7_INC_COMMON0_H + + + +// #define Z7_REDEFINE_NULL + +#if defined(Z7_REDEFINE_NULL) /* && (!defined(__clang__) || defined(_MSC_VER)) */ + +// NULL is defined in +#include +#undef NULL + +#ifdef __cplusplus + #if defined (__cplusplus) && __cplusplus >= 201103L \ + || (defined(_MSC_VER) && _MSC_VER >= 1800) + #define NULL nullptr + #else + #define NULL 0 + #endif +#else + #define NULL ((void *)0) +#endif + +#else // Z7_REDEFINE_NULL + +#if defined(__clang__) && __clang_major__ >= 5 +#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant" +#endif + +#endif // Z7_REDEFINE_NULL + +// for precompiler: +// #include "MyWindows.h" diff --git a/libs/archive/thirdparty/CPP/Common/MyGuidDef.h b/libs/archive/thirdparty/CPP/Common/MyGuidDef.h new file mode 100644 index 0000000..3aa5266 --- /dev/null +++ b/libs/archive/thirdparty/CPP/Common/MyGuidDef.h @@ -0,0 +1,63 @@ +// Common/MyGuidDef.h + +// #pragma message "Common/MyGuidDef.h" + +#ifndef GUID_DEFINED +#define GUID_DEFINED + +// #pragma message "GUID_DEFINED" + +#include "MyTypes.h" + +typedef struct { + UInt32 Data1; + UInt16 Data2; + UInt16 Data3; + Byte Data4[8]; +} GUID; + +#ifdef __cplusplus +#define REFGUID const GUID & +#else +#define REFGUID const GUID * +#endif + +// typedef GUID IID; +typedef GUID CLSID; + +#define REFCLSID REFGUID +#define REFIID REFGUID + +#ifdef __cplusplus +inline int operator==(REFGUID g1, REFGUID g2) +{ + for (unsigned i = 0; i < sizeof(g1); i++) + if (((const Byte *)&g1)[i] != ((const Byte *)&g2)[i]) + return 0; + return 1; +} +inline int operator!=(REFGUID g1, REFGUID g2) { return !(g1 == g2); } +#endif + +#endif // GUID_DEFINED + +#ifndef EXTERN_C +#ifdef __cplusplus + #define EXTERN_C extern "C" +#else + #define EXTERN_C extern +#endif +#endif + +#ifdef DEFINE_GUID +#undef DEFINE_GUID +#endif + +#ifdef INITGUID + #define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ + EXTERN_C const GUID name; \ + EXTERN_C const GUID name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } } +#else + #define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ + EXTERN_C const GUID name +#endif diff --git a/libs/archive/thirdparty/CPP/Common/MyTypes.h b/libs/archive/thirdparty/CPP/Common/MyTypes.h new file mode 100644 index 0000000..eadc9a4 --- /dev/null +++ b/libs/archive/thirdparty/CPP/Common/MyTypes.h @@ -0,0 +1,38 @@ +// Common/MyTypes.h + +#ifndef ZIP7_INC_COMMON_MY_TYPES_H +#define ZIP7_INC_COMMON_MY_TYPES_H + +#include "Common0.h" +#include "../../C/7zTypes.h" + +// typedef int HRes; +// typedef HRESULT HRes; + +struct CBoolPair +{ + bool Val; + bool Def; + + CBoolPair(): Val(false), Def(false) {} + + void Init() + { + Val = false; + Def = false; + } + + void SetTrueTrue() + { + Val = true; + Def = true; + } + + void SetVal_as_Defined(bool val) + { + Val = val; + Def = true; + } +}; + +#endif diff --git a/libs/archive/thirdparty/CPP/Common/MyUnknown.h b/libs/archive/thirdparty/CPP/Common/MyUnknown.h new file mode 100644 index 0000000..75ee96f --- /dev/null +++ b/libs/archive/thirdparty/CPP/Common/MyUnknown.h @@ -0,0 +1,8 @@ +// MyUnknown.h + +#ifndef ZIP7_INC_MY_UNKNOWN_H +#define ZIP7_INC_MY_UNKNOWN_H + +#include "MyWindows.h" + +#endif diff --git a/libs/archive/thirdparty/CPP/Common/MyWindows.cpp b/libs/archive/thirdparty/CPP/Common/MyWindows.cpp new file mode 100644 index 0000000..a1dfbef --- /dev/null +++ b/libs/archive/thirdparty/CPP/Common/MyWindows.cpp @@ -0,0 +1,292 @@ +// MyWindows.cpp + +#include "StdAfx.h" + +#ifndef _WIN32 + +#include +#include +#ifdef __GNUC__ +#include +#endif + +#include "MyWindows.h" + +static inline void *AllocateForBSTR(size_t cb) { return ::malloc(cb); } +static inline void FreeForBSTR(void *pv) { ::free(pv);} + +/* Win32 uses DWORD (32-bit) type to store size of string before (OLECHAR *) string. + We must select CBstrSizeType for another systems (not Win32): + + if (CBstrSizeType is UINT32), + then we support only strings smaller than 4 GB. + Win32 version always has that limitation. + + if (CBstrSizeType is UINT), + (UINT can be 16/32/64-bit) + We can support strings larger than 4 GB (if UINT is 64-bit), + but sizeof(UINT) can be different in parts compiled by + different compilers/settings, + and we can't send such BSTR strings between such parts. +*/ + +typedef UINT32 CBstrSizeType; +// typedef UINT CBstrSizeType; + +#define k_BstrSize_Max 0xFFFFFFFF +// #define k_BstrSize_Max UINT_MAX +// #define k_BstrSize_Max ((UINT)(INT)-1) + +BSTR SysAllocStringByteLen(LPCSTR s, UINT len) +{ + /* Original SysAllocStringByteLen in Win32 maybe fills only unaligned null OLECHAR at the end. + We provide also aligned null OLECHAR at the end. */ + + if (len >= (k_BstrSize_Max - (UINT)sizeof(OLECHAR) - (UINT)sizeof(OLECHAR) - (UINT)sizeof(CBstrSizeType))) + return NULL; + + UINT size = (len + (UINT)sizeof(OLECHAR) + (UINT)sizeof(OLECHAR) - 1) & ~((UINT)sizeof(OLECHAR) - 1); + void *p = AllocateForBSTR(size + (UINT)sizeof(CBstrSizeType)); + if (!p) + return NULL; + *(CBstrSizeType *)p = (CBstrSizeType)len; + BSTR bstr = (BSTR)((CBstrSizeType *)p + 1); + if (s) + memcpy(bstr, s, len); + for (; len < size; len++) + ((Byte *)bstr)[len] = 0; + return bstr; +} + +BSTR SysAllocStringLen(const OLECHAR *s, UINT len) +{ + if (len >= (k_BstrSize_Max - (UINT)sizeof(OLECHAR) - (UINT)sizeof(CBstrSizeType)) / (UINT)sizeof(OLECHAR)) + return NULL; + + UINT size = len * (UINT)sizeof(OLECHAR); + void *p = AllocateForBSTR(size + (UINT)sizeof(CBstrSizeType) + (UINT)sizeof(OLECHAR)); + if (!p) + return NULL; + *(CBstrSizeType *)p = (CBstrSizeType)size; + BSTR bstr = (BSTR)((CBstrSizeType *)p + 1); + if (s) + memcpy(bstr, s, size); + bstr[len] = 0; + return bstr; +} + +BSTR SysAllocString(const OLECHAR *s) +{ + if (!s) + return NULL; + const OLECHAR *s2 = s; + while (*s2 != 0) + s2++; + return SysAllocStringLen(s, (UINT)(s2 - s)); +} + +void SysFreeString(BSTR bstr) +{ + if (bstr) + FreeForBSTR((CBstrSizeType *)(void *)bstr - 1); +} + +UINT SysStringByteLen(BSTR bstr) +{ + if (!bstr) + return 0; + return *((CBstrSizeType *)(void *)bstr - 1); +} + +UINT SysStringLen(BSTR bstr) +{ + if (!bstr) + return 0; + return *((CBstrSizeType *)(void *)bstr - 1) / (UINT)sizeof(OLECHAR); +} + + +HRESULT VariantClear(VARIANTARG *prop) +{ + if (prop->vt == VT_BSTR) + SysFreeString(prop->bstrVal); + prop->vt = VT_EMPTY; + return S_OK; +} + +HRESULT VariantCopy(VARIANTARG *dest, const VARIANTARG *src) +{ + HRESULT res = ::VariantClear(dest); + if (res != S_OK) + return res; + if (src->vt == VT_BSTR) + { + dest->bstrVal = SysAllocStringByteLen((LPCSTR)src->bstrVal, + SysStringByteLen(src->bstrVal)); + if (!dest->bstrVal) + return E_OUTOFMEMORY; + dest->vt = VT_BSTR; + } + else + *dest = *src; + return S_OK; +} + +LONG CompareFileTime(const FILETIME* ft1, const FILETIME* ft2) +{ + if (ft1->dwHighDateTime < ft2->dwHighDateTime) return -1; + if (ft1->dwHighDateTime > ft2->dwHighDateTime) return 1; + if (ft1->dwLowDateTime < ft2->dwLowDateTime) return -1; + if (ft1->dwLowDateTime > ft2->dwLowDateTime) return 1; + return 0; +} + +DWORD GetLastError() +{ + return (DWORD)errno; +} + +void SetLastError(DWORD dw) +{ + errno = (int)dw; +} + + +static LONG TIME_GetBias() +{ + time_t utc = time(NULL); + struct tm *ptm = localtime(&utc); + int localdaylight = ptm->tm_isdst; /* daylight for local timezone */ + ptm = gmtime(&utc); + ptm->tm_isdst = localdaylight; /* use local daylight, not that of Greenwich */ + LONG bias = (int)(mktime(ptm)-utc); + return bias; +} + +#define TICKS_PER_SEC 10000000 +/* +#define SECS_PER_DAY (24 * 60 * 60) +#define SECS_1601_TO_1970 ((369 * 365 + 89) * (UInt64)SECS_PER_DAY) +#define TICKS_1601_TO_1970 (SECS_1601_TO_1970 * TICKS_PER_SEC) +*/ + +#define GET_TIME_64(pft) ((pft)->dwLowDateTime | ((UInt64)(pft)->dwHighDateTime << 32)) + +#define SET_FILETIME(ft, v64) \ + (ft)->dwLowDateTime = (DWORD)v64; \ + (ft)->dwHighDateTime = (DWORD)(v64 >> 32); + + +BOOL WINAPI FileTimeToLocalFileTime(const FILETIME *fileTime, FILETIME *localFileTime) +{ + UInt64 v = GET_TIME_64(fileTime); + v = (UInt64)((Int64)v - (Int64)TIME_GetBias() * TICKS_PER_SEC); + SET_FILETIME(localFileTime, v) + return TRUE; +} + +BOOL WINAPI LocalFileTimeToFileTime(const FILETIME *localFileTime, FILETIME *fileTime) +{ + UInt64 v = GET_TIME_64(localFileTime); + v = (UInt64)((Int64)v + (Int64)TIME_GetBias() * TICKS_PER_SEC); + SET_FILETIME(fileTime, v) + return TRUE; +} + +/* +VOID WINAPI GetSystemTimeAsFileTime(FILETIME *ft) +{ + UInt64 t = 0; + timeval tv; + if (gettimeofday(&tv, NULL) == 0) + { + t = tv.tv_sec * (UInt64)TICKS_PER_SEC + TICKS_1601_TO_1970; + t += tv.tv_usec * 10; + } + SET_FILETIME(ft, t) +} +*/ + +DWORD WINAPI GetTickCount(VOID) +{ + #ifndef _WIN32 + // gettimeofday() doesn't work in some MINGWs by unknown reason + timeval tv; + if (gettimeofday(&tv, NULL) == 0) + { + // tv_sec and tv_usec are (long) + return (DWORD)((UInt64)(Int64)tv.tv_sec * (UInt64)1000 + (UInt64)(Int64)tv.tv_usec / 1000); + } + #endif + return (DWORD)time(NULL) * 1000; +} + + +#define PERIOD_4 (4 * 365 + 1) +#define PERIOD_100 (PERIOD_4 * 25 - 1) +#define PERIOD_400 (PERIOD_100 * 4 + 1) + +BOOL WINAPI FileTimeToSystemTime(const FILETIME *ft, SYSTEMTIME *st) +{ + UInt32 v; + UInt64 v64 = GET_TIME_64(ft); + v64 /= 10000; + st->wMilliseconds = (WORD)(v64 % 1000); v64 /= 1000; + st->wSecond = (WORD)(v64 % 60); v64 /= 60; + st->wMinute = (WORD)(v64 % 60); v64 /= 60; + v = (UInt32)v64; + st->wHour = (WORD)(v % 24); v /= 24; + + // 1601-01-01 was Monday + st->wDayOfWeek = (WORD)((v + 1) % 7); + + UInt32 leaps, year, day, mon; + leaps = (3 * ((4 * v + (365 - 31 - 28) * 4 + 3) / PERIOD_400) + 3) / 4; + v += 28188 + leaps; + // leaps - the number of exceptions from PERIOD_4 rules starting from 1600-03-01 + // (1959 / 64) - converts day from 03-01 to month + year = (20 * v - 2442) / (5 * PERIOD_4); + day = v - (year * PERIOD_4) / 4; + mon = (64 * day) / 1959; + st->wDay = (WORD)(day - (1959 * mon) / 64); + mon -= 1; + year += 1524; + if (mon > 12) + { + mon -= 12; + year++; + } + st->wMonth = (WORD)mon; + st->wYear = (WORD)year; + + /* + unsigned year, mon; + unsigned char ms[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + unsigned t; + + year = (WORD)(1601 + v / PERIOD_400 * 400); + v %= PERIOD_400; + + t = v / PERIOD_100; if (t == 4) t = 3; year += t * 100; v -= t * PERIOD_100; + t = v / PERIOD_4; if (t == 25) t = 24; year += t * 4; v -= t * PERIOD_4; + t = v / 365; if (t == 4) t = 3; year += t; v -= t * 365; + + st->wYear = (WORD)year; + + if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) + ms[1] = 29; + for (mon = 0;; mon++) + { + unsigned d = ms[mon]; + if (v < d) + break; + v -= d; + } + st->wDay = (WORD)(v + 1); + st->wMonth = (WORD)(mon + 1); + */ + + return TRUE; +} + +#endif diff --git a/libs/archive/thirdparty/CPP/Common/MyWindows.h b/libs/archive/thirdparty/CPP/Common/MyWindows.h new file mode 100644 index 0000000..da5370b --- /dev/null +++ b/libs/archive/thirdparty/CPP/Common/MyWindows.h @@ -0,0 +1,325 @@ +// MyWindows.h + +#ifdef Z7_DEFINE_GUID +#undef Z7_DEFINE_GUID +#endif + +#ifdef INITGUID + #define Z7_DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ + EXTERN_C const GUID name; \ + EXTERN_C const GUID name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } } +#else + #define Z7_DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ + EXTERN_C const GUID name +#endif + + +#ifndef ZIP7_INC_MY_WINDOWS_H +#define ZIP7_INC_MY_WINDOWS_H + +#ifdef _WIN32 + +#include "../../C/7zWindows.h" + +#else // _WIN32 + +#include // for wchar_t +#include +// #include // for uintptr_t + +#include "../../C/7zTypes.h" +#include "MyGuidDef.h" + +// WINAPI is __stdcall in Windows-MSVC in windef.h +#define WINAPI + +typedef char CHAR; +typedef unsigned char UCHAR; + +#undef BYTE +typedef unsigned char BYTE; + +typedef short SHORT; +typedef unsigned short USHORT; + +#undef WORD +typedef unsigned short WORD; +typedef short VARIANT_BOOL; + +#define LOWORD(l) ((WORD)((DWORD_PTR)(l) & 0xffff)) +#define HIWORD(l) ((WORD)((DWORD_PTR)(l) >> 16)) + +// MS uses long for BOOL, but long is 32-bit in MS. So we use int. +// typedef long BOOL; +typedef int BOOL; + +#ifndef FALSE + #define FALSE 0 + #define TRUE 1 +#endif + +// typedef size_t ULONG_PTR; +// typedef size_t DWORD_PTR; +// typedef uintptr_t UINT_PTR; +// typedef ptrdiff_t UINT_PTR; + +typedef Int64 LONGLONG; +typedef UInt64 ULONGLONG; + +typedef struct { LONGLONG QuadPart; } LARGE_INTEGER; +typedef struct { ULONGLONG QuadPart; } ULARGE_INTEGER; + +typedef const CHAR *LPCSTR; +typedef CHAR TCHAR; +typedef const TCHAR *LPCTSTR; +typedef wchar_t WCHAR; +typedef WCHAR OLECHAR; +typedef const WCHAR *LPCWSTR; +typedef OLECHAR *BSTR; +typedef const OLECHAR *LPCOLESTR; +typedef OLECHAR *LPOLESTR; + +typedef struct +{ + DWORD dwLowDateTime; + DWORD dwHighDateTime; +} FILETIME; + +#define SUCCEEDED(hr) ((HRESULT)(hr) >= 0) +#define FAILED(hr) ((HRESULT)(hr) < 0) +typedef ULONG PROPID; +typedef LONG SCODE; + + +#define S_OK ((HRESULT)0x00000000L) +#define S_FALSE ((HRESULT)0x00000001L) +#define E_NOTIMPL ((HRESULT)0x80004001L) +#define E_NOINTERFACE ((HRESULT)0x80004002L) +#define E_ABORT ((HRESULT)0x80004004L) +#define E_FAIL ((HRESULT)0x80004005L) +#define STG_E_INVALIDFUNCTION ((HRESULT)0x80030001L) +#define CLASS_E_CLASSNOTAVAILABLE ((HRESULT)0x80040111L) + + +#ifdef _MSC_VER +#define STDMETHODCALLTYPE __stdcall +#define STDAPICALLTYPE __stdcall +#else +// do we need __export here? +#define STDMETHODCALLTYPE +#define STDAPICALLTYPE +#endif + +#define STDAPI EXTERN_C HRESULT STDAPICALLTYPE + +#ifndef DECLSPEC_NOTHROW +#define DECLSPEC_NOTHROW Z7_DECLSPEC_NOTHROW +#endif + +#ifndef DECLSPEC_NOVTABLE +#define DECLSPEC_NOVTABLE Z7_DECLSPEC_NOVTABLE +#endif + +#ifndef COM_DECLSPEC_NOTHROW +#ifdef COM_STDMETHOD_CAN_THROW + #define COM_DECLSPEC_NOTHROW +#else + #define COM_DECLSPEC_NOTHROW DECLSPEC_NOTHROW +#endif +#endif + +#define DECLARE_INTERFACE(iface) struct DECLSPEC_NOVTABLE iface +#define DECLARE_INTERFACE_(iface, baseiface) struct DECLSPEC_NOVTABLE iface : public baseiface + +#define STDMETHOD_(t, f) virtual COM_DECLSPEC_NOTHROW t STDMETHODCALLTYPE f +#define STDMETHOD(f) STDMETHOD_(HRESULT, f) +#define STDMETHODIMP_(t) COM_DECLSPEC_NOTHROW t STDMETHODCALLTYPE +#define STDMETHODIMP STDMETHODIMP_(HRESULT) + + +#define PURE = 0 + +// #define MIDL_INTERFACE(x) struct + + +#ifdef __cplusplus + +/* + p7zip and 7-Zip before v23 used virtual destructor in IUnknown, + if _WIN32 is not defined. + It used virtual destructor, because some compilers don't like virtual + interfaces without virtual destructor. + IUnknown in Windows (_WIN32) doesn't use virtual destructor in IUnknown. + We still can define Z7_USE_VIRTUAL_DESTRUCTOR_IN_IUNKNOWN here, + if we want to be compatible with old plugin interface of p7zip and 7-Zip before v23. + +v23: + In new 7-Zip v23 we try to be more compatible with original IUnknown from _WIN32. + So we do not define Z7_USE_VIRTUAL_DESTRUCTOR_IN_IUNKNOWN here, +*/ +// #define Z7_USE_VIRTUAL_DESTRUCTOR_IN_IUNKNOWN + +#ifdef Z7_USE_VIRTUAL_DESTRUCTOR_IN_IUNKNOWN +#if defined(__clang__) +#pragma GCC diagnostic ignored "-Winconsistent-missing-destructor-override" +#endif +#endif + +Z7_PURE_INTERFACES_BEGIN + +DEFINE_GUID(IID_IUnknown, +0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); +struct IUnknown +{ + STDMETHOD(QueryInterface) (REFIID iid, void **outObject) =0; + STDMETHOD_(ULONG, AddRef)() =0; + STDMETHOD_(ULONG, Release)() =0; + #ifdef Z7_USE_VIRTUAL_DESTRUCTOR_IN_IUNKNOWN + virtual ~IUnknown() {} + #endif +}; + +typedef IUnknown *LPUNKNOWN; + +Z7_PURE_INTERFACES_END + +#endif // __cplusplus + +#define VARIANT_TRUE ((VARIANT_BOOL)-1) +#define VARIANT_FALSE ((VARIANT_BOOL)0) + +enum VARENUM +{ + VT_EMPTY = 0, + VT_NULL = 1, + VT_I2 = 2, + VT_I4 = 3, + VT_R4 = 4, + VT_R8 = 5, + VT_CY = 6, + VT_DATE = 7, + VT_BSTR = 8, + VT_DISPATCH = 9, + VT_ERROR = 10, + VT_BOOL = 11, + VT_VARIANT = 12, + VT_UNKNOWN = 13, + VT_DECIMAL = 14, + + VT_I1 = 16, + VT_UI1 = 17, + VT_UI2 = 18, + VT_UI4 = 19, + VT_I8 = 20, + VT_UI8 = 21, + VT_INT = 22, + VT_UINT = 23, + VT_VOID = 24, + VT_HRESULT = 25, + VT_FILETIME = 64 +}; + +typedef unsigned short VARTYPE; +typedef WORD PROPVAR_PAD1; +typedef WORD PROPVAR_PAD2; +typedef WORD PROPVAR_PAD3; + +typedef struct tagPROPVARIANT +{ + VARTYPE vt; + PROPVAR_PAD1 wReserved1; + PROPVAR_PAD2 wReserved2; + PROPVAR_PAD3 wReserved3; + union + { + CHAR cVal; + UCHAR bVal; + SHORT iVal; + USHORT uiVal; + LONG lVal; + ULONG ulVal; + INT intVal; + UINT uintVal; + LARGE_INTEGER hVal; + ULARGE_INTEGER uhVal; + VARIANT_BOOL boolVal; + SCODE scode; + FILETIME filetime; + BSTR bstrVal; + }; +} PROPVARIANT; + +typedef PROPVARIANT tagVARIANT; +typedef tagVARIANT VARIANT; +typedef VARIANT VARIANTARG; + +EXTERN_C HRESULT VariantClear(VARIANTARG *prop); +EXTERN_C HRESULT VariantCopy(VARIANTARG *dest, const VARIANTARG *src); + +typedef struct tagSTATPROPSTG +{ + LPOLESTR lpwstrName; + PROPID propid; + VARTYPE vt; +} STATPROPSTG; + +EXTERN_C BSTR SysAllocStringByteLen(LPCSTR psz, UINT len); +EXTERN_C BSTR SysAllocStringLen(const OLECHAR *sz, UINT len); +EXTERN_C BSTR SysAllocString(const OLECHAR *sz); +EXTERN_C void SysFreeString(BSTR bstr); +EXTERN_C UINT SysStringByteLen(BSTR bstr); +EXTERN_C UINT SysStringLen(BSTR bstr); + +EXTERN_C DWORD GetLastError(); +EXTERN_C void SetLastError(DWORD dwCode); +EXTERN_C LONG CompareFileTime(const FILETIME* ft1, const FILETIME* ft2); + +EXTERN_C DWORD GetCurrentThreadId(); +EXTERN_C DWORD GetCurrentProcessId(); + +#define MAX_PATH 1024 + +#define CP_ACP 0 +#define CP_OEMCP 1 +#define CP_UTF8 65001 + +typedef enum tagSTREAM_SEEK +{ + STREAM_SEEK_SET = 0, + STREAM_SEEK_CUR = 1, + STREAM_SEEK_END = 2 +} STREAM_SEEK; + + + +typedef struct +{ + WORD wYear; + WORD wMonth; + WORD wDayOfWeek; + WORD wDay; + WORD wHour; + WORD wMinute; + WORD wSecond; + WORD wMilliseconds; +} SYSTEMTIME; + +BOOL WINAPI FileTimeToLocalFileTime(const FILETIME *fileTime, FILETIME *localFileTime); +BOOL WINAPI LocalFileTimeToFileTime(const FILETIME *localFileTime, FILETIME *fileTime); +BOOL WINAPI FileTimeToSystemTime(const FILETIME *fileTime, SYSTEMTIME *systemTime); +// VOID WINAPI GetSystemTimeAsFileTime(FILETIME *systemTimeAsFileTime); + +DWORD GetTickCount(); + + +/* +#define CREATE_NEW 1 +#define CREATE_ALWAYS 2 +#define OPEN_EXISTING 3 +#define OPEN_ALWAYS 4 +#define TRUNCATE_EXISTING 5 +*/ + +#endif // _WIN32 + +#endif diff --git a/libs/archive/thirdparty/CPP/Common/NewHandler.h b/libs/archive/thirdparty/CPP/Common/NewHandler.h new file mode 100644 index 0000000..5ba64b7 --- /dev/null +++ b/libs/archive/thirdparty/CPP/Common/NewHandler.h @@ -0,0 +1,121 @@ +// Common/NewHandler.h + +#ifndef ZIP7_INC_COMMON_NEW_HANDLER_H +#define ZIP7_INC_COMMON_NEW_HANDLER_H + +/* +NewHandler.h and NewHandler.cpp allows to solve problem with compilers that +don't throw exception in operator new(). + +This file must be included before any code that uses operators new() or delete() +and you must compile and link "NewHandler.cpp", if you use some old MSVC compiler. + +DOCs: + Since ISO C++98, operator new throws std::bad_alloc when memory allocation fails. + MSVC 6.0 returned a null pointer on an allocation failure. + Beginning in VS2002, operator new conforms to the standard and throws on failure. + + By default, the compiler also generates defensive null checks to prevent + these older-style allocators from causing an immediate crash on failure. + The /Zc:throwingNew option tells the compiler to leave out these null checks, + on the assumption that all linked memory allocators conform to the standard. + +The operator new() in some MSVC versions doesn't throw exception std::bad_alloc. +MSVC 6.0 (_MSC_VER == 1200) doesn't throw exception. +The code produced by some another MSVC compilers also can be linked +to library that doesn't throw exception. +We suppose that code compiled with VS2015+ (_MSC_VER >= 1900) throws exception std::bad_alloc. +For older _MSC_VER versions we redefine operator new() and operator delete(). +Our version of operator new() throws CNewException() exception on failure. + +It's still allowed to use redefined version of operator new() from "NewHandler.cpp" +with any compiler. 7-Zip's code can work with std::bad_alloc and CNewException() exceptions. +But if you use some additional code (outside of 7-Zip's code), you must check +that redefined version of operator new() is not problem for your code. +*/ + +#include + +#ifdef _WIN32 +// We can compile my_new and my_delete with _fastcall +/* +void * my_new(size_t size); +void my_delete(void *p) throw(); +// void * my_Realloc(void *p, size_t newSize, size_t oldSize); +*/ +#endif + + +#if defined(_MSC_VER) && (_MSC_VER < 1600) + // If you want to use default operator new(), you can disable the following line + #define Z7_REDEFINE_OPERATOR_NEW +#endif + + +#ifdef Z7_REDEFINE_OPERATOR_NEW + +// std::bad_alloc can require additional DLL dependency. +// So we don't define CNewException as std::bad_alloc here. + +class CNewException {}; + +void * +#ifdef _MSC_VER +__cdecl +#endif +operator new(size_t size); + +/* +#if 0 && defined(_MSC_VER) && _MSC_VER == 1600 + #define Z7_OPERATOR_DELETE_SPEC_THROW0 +#else + #define Z7_OPERATOR_DELETE_SPEC_THROW0 throw() +#endif +*/ +#if defined(_MSC_VER) && _MSC_VER == 1600 +#pragma warning(push) +#pragma warning(disable : 4986) // 'operator delete': exception specification does not match previous declaration +#endif + +void +#ifdef _MSC_VER +__cdecl +#endif +operator delete(void *p) throw(); + +void +#ifdef _MSC_VER +__cdecl +#endif +operator delete(void *p, size_t n) throw(); + +#if defined(_MSC_VER) && _MSC_VER == 1600 +#pragma warning(pop) +#endif + + +#else + +#include + +#define CNewException std::bad_alloc + +#endif + +/* +#ifdef _WIN32 +void * +#ifdef _MSC_VER +__cdecl +#endif +operator new[](size_t size); + +void +#ifdef _MSC_VER +__cdecl +#endif +operator delete[](void *p) throw(); +#endif +*/ + +#endif diff --git a/libs/archive/thirdparty/CPP/Common/StdAfx.h b/libs/archive/thirdparty/CPP/Common/StdAfx.h new file mode 100644 index 0000000..a5228b0 --- /dev/null +++ b/libs/archive/thirdparty/CPP/Common/StdAfx.h @@ -0,0 +1,8 @@ +// StdAfx.h + +#ifndef ZIP7_INC_STDAFX_H +#define ZIP7_INC_STDAFX_H + +#include "Common.h" + +#endif diff --git a/libs/archive/vcpkg.json b/libs/archive/vcpkg.json new file mode 100644 index 0000000..5367449 --- /dev/null +++ b/libs/archive/vcpkg.json @@ -0,0 +1,10 @@ +{ + "dependencies": ["7zip"], + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43" + } + } +} diff --git a/libs/basic_games/.github/workflows/build.yml b/libs/basic_games/.github/workflows/build.yml new file mode 100644 index 0000000..ef00467 --- /dev/null +++ b/libs/basic_games/.github/workflows/build.yml @@ -0,0 +1,44 @@ +name: Build Basic Games Plugin + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + runs-on: windows-2022 + steps: + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Set environmental variables + shell: bash + run: | + echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> $GITHUB_ENV + + - uses: actions/checkout@v4 + with: + path: build + + - name: Configure Basic Games Plugin build + working-directory: ${{ github.workspace }}/build + shell: pwsh + run: | + cmake --preset vs2022-windows "-DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/install" "-DVCPKG_MANIFEST_FEATURES=standalone" + + - name: Build Basic Games Plugin + working-directory: ${{ github.workspace }}/build + run: cmake --build vsbuild --config RelWithDebInfo + + - name: Install Basic Games Plugin + working-directory: ${{ github.workspace }}/build + run: cmake --install vsbuild --config RelWithDebInfo + + - name: Upload Basic Games Plugin artifact + uses: actions/upload-artifact@master + with: + name: basic_games + path: ${{ github.workspace }}/install/bin/plugins diff --git a/libs/basic_games/.github/workflows/linters.yml b/libs/basic_games/.github/workflows/linters.yml new file mode 100644 index 0000000..711a3a7 --- /dev/null +++ b/libs/basic_games/.github/workflows/linters.yml @@ -0,0 +1,24 @@ +name: linters + +on: [push, pull_request] + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + path: "basic_games" + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.12 + - uses: abatilo/actions-poetry@v2 + - name: Install + run: | + cd basic_games + poetry --no-root install + - name: Lint + run: | + cd basic_games + poetry run poe lint diff --git a/libs/basic_games/.gitignore b/libs/basic_games/.gitignore new file mode 100644 index 0000000..910d2b5 --- /dev/null +++ b/libs/basic_games/.gitignore @@ -0,0 +1,12 @@ +.mypy_cache +__pycache__ +.venv +venv +.tox +.vscode +.idea + +# Mob / Umbrella +mob.log +vsbuild +lib diff --git a/libs/basic_games/.pre-commit-config.yaml b/libs/basic_games/.pre-commit-config.yaml new file mode 100644 index 0000000..ae8a99a --- /dev/null +++ b/libs/basic_games/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + - id: mixed-line-ending + args: [--fix=lf] + - id: check-case-conflict + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.10 # must match pyproject.toml + hooks: + - id: ruff + args: [--extend-select, I, --fix] + - id: ruff-format + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/basic_games/CMakeLists.txt b/libs/basic_games/CMakeLists.txt new file mode 100644 index 0000000..288c2a4 --- /dev/null +++ b/libs/basic_games/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) + +project(basic_games LANGUAGES NONE) + +find_package(mo2-cmake CONFIG REQUIRED) + +add_custom_target(basic_games ALL) +mo2_configure_python(basic_games MODULE TRANSLATIONS OFF) diff --git a/libs/basic_games/CMakePresets.json b/libs/basic_games/CMakePresets.json new file mode 100644 index 0000000..acc6d97 --- /dev/null +++ b/libs/basic_games/CMakePresets.json @@ -0,0 +1,17 @@ +{ + "configurePresets": [ + { + "binaryDir": "${sourceDir}/vsbuild", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "generator": "Visual Studio 17 2022", + "name": "vs2022-windows" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/basic_games/LICENSE b/libs/basic_games/LICENSE new file mode 100644 index 0000000..45ca39c --- /dev/null +++ b/libs/basic_games/LICENSE @@ -0,0 +1,7 @@ +Copyright 2020 Holt59 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/libs/basic_games/README.md b/libs/basic_games/README.md new file mode 100644 index 0000000..2c0e98b --- /dev/null +++ b/libs/basic_games/README.md @@ -0,0 +1,233 @@ +# Mod Organizer 2 - Simple Games Plugin + +Mod Organizer 2 meta-plugin to make creating game plugins easier and faster. + +## Why? + +In order to create a MO2 game plugin, one must implement the `IPluginGame` interface. +This interface was initially designed for Bethesda games such as the Elder Scrolls or +Fallout series and thus contains a lot of things that are irrelevant for most games. + +The goal of this meta-plugin is to allow creating game plugins for "basic" games by +providing a very simple python class. + +## How to install? + +Download the archive for your MO2 version and extract it directly into your MO2 `plugins` folder. + +- Mod Organizer **2.3.2**: [Download](https://github.com/ModOrganizer2/modorganizer-basic_games/releases/download/v0.0.3/basic_games-0.0.3.zip) + and extract in your `plugins/` folder (see below). +- Mod Organizer **2.4**: Basic games is included in Mod Organizer 2.4. + - If you want to use new game plugins that have not been included in the + release, [download the latest archive](https://github.com/ModOrganizer2/modorganizer-basic_games/archive/master.zip) and extract the files + in the existing `basic_games` folder, overwriting existing files. + +**Important:** Extract the *folder* in your `plugins` folder, not the individual files. Your +`plugins` folder should look like this: + +```text +dlls/ +plugins/ + data/ + basic_games/ + games/ + __init__.py + ... + __init__.py + basic_game.py + ... + bsa_extractor.dll + ... +ModOrganizer.exe +``` + +You can rename `modorganizer-basic_games-xxx` to whatever you want (e.g., `basic_games`). + +## Supported games + +| Game | Author | File | Extras | +|------|--------|------|--------| +| The Binding of Isaac: Rebirth — [STEAM](https://store.steampowered.com/app/250900/The_Binding_of_Isaac_Rebirth/) |[EzioTheDeadPoet](https://github.com/EzioTheDeadPoet)|[game_thebindingofisaacrebirth.py](games/game_thebindingofisaacrebirth.py)|
  • profile specific ini file
| +| Control — [STEAM](https://store.steampowered.com/app/870780/Control_Ultimate_Edition/) / [GOG](https://www.gog.com/game/control_ultimate_edition) / [EGS](https://www.epicgames.com/store/p/control) | [Zash](https://github.com/ZashIn) | [game_control.py](games/game_control.py) | | +| Darkest Dungeon — [GOG](https://www.gog.com/game/darkest_dungeon) / [STEAM](https://store.steampowered.com/app/262060/Darkest_Dungeon/) | [erri120](https://github.com/erri120) | [game_darkestdungeon.py](games/game_darkestdungeon.py) |
  • save slot parsing
  • mod data checker
| +| Dark Messiah of Might & Magic — [STEAM](https://store.steampowered.com/app/2100/Dark_Messiah_of_Might__Magic/) | [Holt59](https://github.com/holt59/) | [game_darkmessiahofmightandmagic.py](games/game_darkmessiahofmightandmagic.py) |
  • save game preview
| +| Dark Souls — [STEAM](https://store.steampowered.com/app/211420/DARK_SOULS_Prepare_To_Die_Edition/) | [Holt59](https://github.com/holt59/) | [game_darksouls.py](games/game_darkestdungeon.py) | | +| Divinity: Original Sin (Classic) — [STEAM](https://store.steampowered.com/app/230230/Divinity_Original_Sin_Classic/) | [LostDragonist](https://github.com/LostDragonist/) | [game_divinityoriginalsin.py](games/game_divinityoriginalsin.py) |
  • save game preview
| +| Divinity: Original Sin (Enhanced Edition) — [STEAM](https://store.steampowered.com/app/373420/Divinity_Original_Sin__Enhanced_Edition/) | [LostDragonist](https://github.com/LostDragonist/) | [game_divinityoriginalsinee.py](games/game_divinityoriginalsinee.py) |
  • save game preview
  • mod data checker
| +| Dragon's Dogma: Dark Arisen — [GOG](https://www.gog.com/game/dragons_dogma_dark_arisen) / [STEAM](https://store.steampowered.com/app/367500/Dragons_Dogma_Dark_Arisen/) | [EzioTheDeadPoet](https://github.com/EzioTheDeadPoet) | [game_dragonsdogmadarkarisen.py](games/game_dragonsdogmadarkarisen.py) | | +| Dungeon Siege II — [GOG](https://www.gog.com/game/dungeon_siege_collection) / [STEAM](https://store.steampowered.com/app/39200/Dungeon_Siege_II/) | [Holt59](https://github.com/holt59/) | [game_dungeonsiege2.py](games/game_dungeonsiege2.py) |
  • mod data checker
| +| Kingdom Come: Deliverance — [GOG](https://www.gog.com/game/kingdom_come_deliverance) / [STEAM](https://store.steampowered.com/app/379430/Kingdom_Come_Deliverance/) / [Epic](https://store.epicgames.com/p/kingdom-come-deliverance) | [Silencer711](https://github.com/Silencer711) | [game_kingdomcomedeliverance.py](games/game_kingdomcomedeliverance.py) |
  • profile specific cfg files
| +| METAL GEAR SOLID 2: Sons of Liberty — [STEAM](https://store.steampowered.com/app/2131640/METAL_GEAR_SOLID_2_Sons_of_Liberty__Master_Collection_Version/)|[AkiraJkr](https://github.com/AkiraJkr)|[game_metalgearsolid2mc.py](games/game_metalgearsolid2mc.py)| | +| METAL GEAR SOLID 3: Snake Eater — [STEAM](https://store.steampowered.com/app/2131650/METAL_GEAR_SOLID_3_Snake_Eater__Master_Collection_Version/)|[AkiraJkr](https://github.com/AkiraJkr)|[game_metalgearsolid3mc.py](games/game_metalgearsolid3mc.py)| | +| Mirror's Edge — [GOG](https://www.gog.com/game/mirrors_edge) / [STEAM](https://store.steampowered.com/app/17410/Mirrors_Edge)|[EzioTheDeadPoet](https://eziothedeadpoet.github.io/AboutMe/)|[game_mirrorsedge.py](games/game_mirrorsedge.py)| | +| Mount & Blade II: Bannerlord — [GOG](https://www.gog.com/game/mount_blade_ii_bannerlord) / [STEAM](https://store.steampowered.com/app/261550/Mount__Blade_II_Bannerlord/) | [Holt59](https://github.com/holt59/) | [game_mountandblade2.py](games/game_mountandblade2.py) |
  • mod data checker
| +| Need for Speed: High Stakes | [uwx](https://github.com/uwx) | [game_nfshs.py](games/game_nfshs.py) | | +| No Man's Sky - [GOG](https://www.gog.com/game/no_mans_sky) / [Steam](https://store.steampowered.com/app/275850/No_Mans_Sky/)|[EzioTheDeadPoet](https://eziothedeadpoet.github.io/AboutMe/)|[game_nomanssky.py](games/game_nomanssky.py)| | +| S.T.A.L.K.E.R. Anomaly — [MOD](https://www.stalker-anomaly.com/) | [Qudix](https://github.com/Qudix) | [game_stalkeranomaly.py](games/game_stalkeranomaly.py) |
  • mod data checker
| +| Stardew Valley — [GOG](https://www.gog.com/game/stardew_valley) / [STEAM](https://store.steampowered.com/app/413150/Stardew_Valley/) | [Syer10](https://github.com/Syer10), [Holt59](https://github.com/holt59/) | [game_stardewvalley.py](games/game_stardewvalley.py) |
  • mod data checker
| +| STAR WARS™ Empire at War: Gold Pack - [GOG](https://www.gog.com/game/star_wars_empire_at_war_gold_pack) / [STEAM](https://store.steampowered.com/app/32470/) | [erri120](https://github.com/erri120) |
  • Empire at War: [game_starwars-empire-at-war.py](games/game_starwars-empire-at-war.py)
  • Force of Corruption: [game_starwars-empire-at-war-foc.py](games/game_starwars-empire-at-war-foc.py)
| | +| Subnautica — [STEAM](https://store.steampowered.com/app/264710/) / [Epic](https://store.epicgames.com/p/subnautica) | [dekart811](https://github.com/dekart811), [Zash](https://github.com/ZashIn) | [game_subnautica.py](games/game_subnautica.py) |
  • mod data checker
  • save game preview
| +| Subnautica: Below Zero — [STEAM](https://store.steampowered.com/app/848450/) | [dekart811](https://github.com/dekart811), [Zash](https://github.com/ZashIn) | [game_subnautica-below-zero.py](games/game_subnautica-below-zero.py) |
  • mod data checker
  • save game preview
| +| Train Simulator Classic — [STEAM](https://store.steampowered.com/app/24010/) | [Ryan Young](https://github.com/YoRyan) | [game_trainsimulator.py](games/game_trainsimulator.py) | | +| Valheim — [STEAM](https://store.steampowered.com/app/892970/Valheim/) | [Zash](https://github.com/ZashIn) | [game_valheim.py](games/game_valheim.py) |
  • mod data checker
  • overwrite config sync
  • save game support (no preview)
| +| Test Drive Unlimited | [uwx](https://github.com/uwx) | [game_tdu.py](games/game_tdu.py) | | +| Test Drive Unlimited 2 — [STEAM](https://steamcommunity.com/app/9930/) | [uwx](https://github.com/uwx) | [game_tdu2.py](games/game_tdu2.py) | | +| The Witcher: Enhanced Edition - [GOG](https://www.gog.com/game/the_witcher) / [STEAM](https://store.steampowered.com/app/20900/The_Witcher_Enhanced_Edition_Directors_Cut/) | [erri120](https://github.com/erri120) | [game_witcher1.py](games/game_witcher1.py) |
  • save game parsing (no preview)
| +| The Witcher 3: Wild Hunt — [GOG](https://www.gog.com/game/the_witcher_3_wild_hunt) / [STEAM](https://store.steampowered.com/app/292030/The_Witcher_3_Wild_Hunt/) | [Holt59](https://github.com/holt59/) | [game_witcher3.py](games/game_witcher3.py) |
  • save game preview
| +| Tony Hawk's Pro Skater 3 | [uwx](https://github.com/uwx) | [game_thps3.py](games/game_thps3.py) | | +| Tony Hawk's Pro Skater 4 | [uwx](https://github.com/uwx) | [game_thps4.py](games/game_thps4.py) | | +| Tony Hawk's Underground | [uwx](https://github.com/uwx) | [game_thug.py](games/game_thug.py) | | +| Tony Hawk's Underground 2 | [uwx](https://github.com/uwx) | [game_thug2.py](games/game_thug2.py) | | +| Trackmania United Forever — [STEAM](https://store.steampowered.com/app/7200/Trackmania_United_Forever/) | [uwx](https://github.com/uwx) | [game_tmuf.py](games/game_tmuf.py) | | +| Yu-Gi-Oh! Master Duel — [STEAM](https://store.steampowered.com/app/1449850/) | [The Conceptionist](https://github.com/the-conceptionist) & [uwx](https://github.com/uwx) | [game_masterduel.py](games/game_masterduel.py) | | +| Zeus and Poseidon — [GOG](https://www.gog.com/game/zeus_poseidon) / [STEAM](https://store.steampowered.com/app/566050/Zeus__Poseidon/) | [Holt59](https://github.com/holt59/) | [game_zeusandpoiseidon.py](games/game_zeusandpoiseidon.py) |
  • mod data checker
| + +## How to add a new game? + +You can create a plugin by providing a python class in the `games` folder. + +**Note:** If your game plugin does not load properly, you should set the log level +to debug and look at the `mo_interface.log` file. + +You need to create a class that inherits `BasicGame` and put it in a `game_XX.py` in `games`. +Below is an example for The Witcher 3 (see also [games/game_witcher3.py](games/game_witcher3.py)): + +```python +from PyQt6.QtCore import QDir +from ..basic_game import BasicGame + + +class Witcher3Game(BasicGame): + + Name = "Witcher 3 Support Plugin" + Author = "Holt59" + Version = "1.0.0a" + + GameName = "The Witcher 3" + GameShortName = "witcher3" + GameBinary = "bin/x64/witcher3.exe" + GameDataPath = "Mods" + GameSaveExtension = "sav" + GameSteamId = 292030 + + def savesDirectory(self): + return QDir(self.documentsDirectory().absoluteFilePath("gamesaves")) +``` + +`BasicGame` inherits `IPluginGame` so you can override methods if you need to. +Each attribute you provide corresponds to a method (e.g., `Version` corresponds +to the `version` method, see the table below). If you override the method, you do +not have to provide the attribute: + +```python +from PyQt6.QtCore import QDir +from ..basic_game import BasicGame + +import mobase + + +class Witcher3Game(BasicGame): + + Name = "Witcher 3 Support Plugin" + Author = "Holt59" + + GameName = "The Witcher 3" + GameShortName = "witcher3" + GameBinary = "bin/x64/witcher3.exe" + GameDataPath = "Mods" + GameSaveExtension = "sav" + GameSteamId = 292030 + + def version(self): + # Don't forget to import mobase! + return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.final) + + def savesDirectory(self): + return QDir(self.documentsDirectory().absoluteFilePath("gamesaves")) +``` + +### List of valid keys + +| Name | Description | `IPluginGame` method | Python | +|------|-------------|----------------------|--------| +| Name | Name of the plugin | `name` | `str` | +| Author | Author of the plugin | `author` | `str` | +| Version | Version of the plugin | `version` | `str` or `mobase.VersionInfo` | +| Description| Description (Optional) | `description` | `str` | +| GameName | Name of the game, as displayed by MO2 | `gameName` | `str` | +| GameShortName | Short name of the game | `gameShortName` | `str` | +| GameNexusName| Nexus name of the game (Optional, default to `GameShortName`) | `gameNexusName` | `str` | +| GameValidShortNames | Other valid short names (Optional) | `validShortNames` | `List[str]` or comma-separated list of values | +| GameNexusId | Nexus ID of the game (Optional) | `nexusGameID` | `str` or `int` | +| GameBinary | Name of the game executable, relative to the game path | `binaryName` | `str` | +| GameLauncher | Name of the game launcher, relative to the game path (Optional) | `getLauncherName` | `str` | +| GameDataPath | Name of the folder containing mods, relative to game folder| `dataDirectory` | | +| GameDocumentsDirectory | Documents directory (Optional) | `documentsDirectory` | `str` or `QDir` | +| GameIniFiles | Config files in documents, for profile specific config (Optional) | `iniFiles` | `str` or `List[str]` | +| GameSavesDirectory | Directory containing saves (Optional, default to `GameDocumentsDirectory`) | `savesDirectory` | `str` or `QDir` | +| GameSaveExtension | Save file extension (Optional) `savegameExtension` | `str` | +| GameSteamId | Steam ID of the game (Optional) | `steamAPPId` | `List[str]` or `str` or `int` | +| GameGogId | GOG ID of the game (Optional) | `gogAPPId` | `List[str]` or `str` or `int` | +| GameOriginManifestIds | Origin Manifest ID of the game (Optional) | `originManifestIds` | `List[str]` or `str` | +| GameOriginWatcherExecutables | Executables to watch for Origin DRM (Optional) | `originWatcherExecutables` | `List[str]` or `str` | +| GameEpicId | Epic ID (`AppName`) of the game (Optional) | `epicAPPId` | `List[str]` or `str` | +| GameEaDesktopId | EA Desktop ID of the game (Optional) | `eaDesktopContentId` | `List[str]` or `str` or `int` | + +You can use the following variables for `str`: + +- `%DOCUMENTS%` will be replaced by the standard *Documents* folder. +- `%GAME_PATH%` will be replaced by the path to the game folder. +- `%GAME_DOCUMENTS%` will be replaced by the value of `GameDocumentsDirectory`. + +## Extra features + +The meta-plugin provides some useful extra feature: + +1. **Automatic Steam, GOG, Origin, Epic Games and EA Desktop detection:** If you provide + Steam, GOG, Origin or Epic IDs for the game (via `GameSteamId`, `GameGogId`, + `GameOriginManifestIds`, `GameEpicId` or `GameEaDesktopId`), the game will be listed + in the list of available games when creating a new MO2 instance (if the game is + installed via Steam, GOG, Origin, Epic Games / Legendary or EA Desktop). +2. **Basic save game preview / metadata** (Python): If you can easily obtain a picture + (file) and/or metadata (like from json) for any saves, you can provide basic save-game + preview by using the `BasicGameSaveGameInfo`. See + [games/game_witcher3.py](games/game_witcher3.py) and + [games/game_bladeandsorcery.py](games/game_bladeandsorcery.py) for more details. +3. **Basic local save games** (Python): profile specific save games, as in [games/game_valheim.py](games/game_valheim.py). +4. **Basic mod data checker** (Python): + Check and fix different mod archive layouts for an automatic installation with the proper + file structure, using simple (glob) patterns via `BasicModDataChecker`. + See [games/game_valheim.py](games/game_valheim.py) and [game_subnautica.py](games/game_subnautica.py) for an example. + +Game IDs can be found here: + +- For Steam on [Steam Database](https://steamdb.info/) +- For GOG on [GOG Database](https://www.gogdb.org/) +- For Origin from `C:\ProgramData\Origin\LocalContent` (.mfst files) +- For Epic Games (`AppName`) from: + - `C:\ProgramData\Epic\EpicGamesLauncher\Data\Manifests\` (.item files) + - or `C:\ProgramData\Epic\EpicGamesLauncher\UnrealEngineLauncher\LauncherInstalled.dat` + - or [Unofficial EGS ID DB](https://erri120.github.io/egs-db/) +- For Legendary (alt. Epic launcher) via command `legendary list-games` + or from: `%USERPROFILE%\.config\legendary\installed.json` +- For EA Desktop from `\\__Installer\installerdata.xml` + +## Contribute + +We recommend using a dedicated Python environment to write a new basic game plugins. + +1. Install the required version of Python --- Currently Python 3.11 (MO2 2.5). +2. Remove the repository at `${MO2_INSTALL}/plugins/basic_games`. +3. Clone this repository at the location of the old plugin ( + `${MO2_INSTALL}/plugins/basic_games`). +4. Place yourself inside the cloned folder and: + + ```bash + # create a virtual environment (recommended) + py -3.11 -m venv .\venv + .\venv\scripts\Activate.ps1 + + # "install" poetry and the development package + pip install poetry + poetry install + ``` diff --git a/libs/basic_games/__init__.py b/libs/basic_games/__init__.py new file mode 100644 index 0000000..dae0a24 --- /dev/null +++ b/libs/basic_games/__init__.py @@ -0,0 +1,96 @@ +# pyright: reportUnboundVariable=false + +import glob +import importlib +import os +import pathlib +import site +import sys +import typing + +from PyQt6.QtCore import qWarning + +from mobase import IPlugin + +from .basic_game import BasicGame +from .basic_game_ini import BasicIniGame + +site.addsitedir(os.path.join(os.path.dirname(__file__), "lib")) + + +BasicGame.setup() + + +def createPlugins(): + # List of game class from python: + game_plugins: typing.List[IPlugin] = [] + + # We are going to list all game plugins: + curpath = os.path.abspath(os.path.dirname(__file__)) + escaped_games_path = glob.escape(os.path.join(curpath, "games")) + + # List all the .ini files: + for file in glob.glob(os.path.join(escaped_games_path, "*.ini")): + game_plugins.append(BasicIniGame(file)) + + # List all the python plugins: + for file in glob.glob(os.path.join(escaped_games_path, "*.py")): + module_p = os.path.relpath(file, os.path.join(curpath, "games")) + if module_p == "__init__.py": + continue + + if module_p == "game_stalkeranomaly.py": + try: + import lzokay # type: ignore # noqa: F401 + except Exception: + # Optional dependency not available on this host. + continue + + # Import the module: + try: + module = importlib.import_module(".games." + module_p[:-3], __package__) + except ImportError as e: + print("Failed to import module {}: {}".format(module_p, e), file=sys.stderr) + continue + except Exception as e: + print("Failed to import module {}: {}".format(module_p, e), file=sys.stderr) + continue + + # Lookup game plugins: + for name in dir(module): + if hasattr(module, name): + obj = getattr(module, name) + if ( + isinstance(obj, type) + and issubclass(obj, BasicGame) + and obj is not BasicGame + ): + try: + game_plugins.append(obj()) + except Exception as e: + print( + "Failed to instantiate {}: {}".format(name, e), + file=sys.stderr, + ) + for path in pathlib.Path(escaped_games_path).rglob("plugins/__init__.py"): + module_path = "." + os.path.relpath(path.parent, curpath).replace(os.sep, ".") + try: + module = importlib.import_module(module_path, __package__) + if hasattr(module, "createPlugins") and callable(module.createPlugins): + try: + plugins: typing.Any = module.createPlugins() + for item in plugins: + if isinstance(item, IPlugin): + game_plugins.append(item) + except TypeError: + pass + if hasattr(module, "createPlugin") and callable(module.createPlugin): + plugin = module.createPlugin() + if isinstance(plugin, IPlugin): + game_plugins.append(plugin) + except ImportError as e: + qWarning(f"Error importing module {module_path}: {e}") + except Exception as e: + qWarning(f"Error calling function createPlugin(s) in {module_path}: {e}") + + return game_plugins diff --git a/libs/basic_games/basic_features/__init__.py b/libs/basic_games/basic_features/__init__.py new file mode 100644 index 0000000..9326dd9 --- /dev/null +++ b/libs/basic_games/basic_features/__init__.py @@ -0,0 +1,10 @@ +from .basic_local_savegames import BasicLocalSavegames +from .basic_mod_data_checker import BasicModDataChecker, GlobPatterns +from .basic_save_game_info import BasicGameSaveGameInfo + +__all__ = [ + "BasicModDataChecker", + "BasicGameSaveGameInfo", + "GlobPatterns", + "BasicLocalSavegames", +] diff --git a/libs/basic_games/basic_features/basic_local_savegames.py b/libs/basic_games/basic_features/basic_local_savegames.py new file mode 100644 index 0000000..e0991f8 --- /dev/null +++ b/libs/basic_games/basic_features/basic_local_savegames.py @@ -0,0 +1,27 @@ +from PyQt6.QtCore import QDir + +import mobase + + +class BasicLocalSavegames(mobase.LocalSavegames): + _game: mobase.IPluginGame + + def __init__(self, game: mobase.IPluginGame): + super().__init__() + self._game = game + + def game_save_dir(self) -> str: + return self._game.savesDirectory().absolutePath() + + def mappings(self, profile_save_dir: QDir): + return [ + mobase.Mapping( + source=profile_save_dir.absolutePath(), + destination=self.game_save_dir(), + is_directory=True, + create_target=True, + ) + ] + + def prepareProfile(self, profile: mobase.IProfile) -> bool: + return profile.localSavesEnabled() diff --git a/libs/basic_games/basic_features/basic_mod_data_checker.py b/libs/basic_games/basic_features/basic_mod_data_checker.py new file mode 100644 index 0000000..1d061eb --- /dev/null +++ b/libs/basic_games/basic_features/basic_mod_data_checker.py @@ -0,0 +1,226 @@ +from __future__ import annotations + +import fnmatch +import re +from dataclasses import dataclass, field +from typing import Iterable, Literal + +import mobase + +from .utils import is_directory + + +class OptionalRegexPattern: + _pattern: re.Pattern[str] | None + + def __init__(self, globs: Iterable[str] | None) -> None: + if globs is None: + self._pattern = None + else: + self._pattern = OptionalRegexPattern.regex_from_glob_list(globs) + + @staticmethod + def regex_from_glob_list(glob_list: Iterable[str]) -> re.Pattern[str]: + """ + Returns a regex pattern form a list of glob patterns. + + Every pattern has a capturing group, so that `match.lastindex - 1` will + give the `glob_list` index. + """ + return re.compile( + "|".join(f"({fnmatch.translate(f)})" for f in glob_list), re.I + ) + + def match(self, value: str) -> bool: + if self._pattern is None: + return False + return bool(self._pattern.match(value)) + + +class RegexPatterns: + """ + Regex patterns for validation in `BasicModDataChecker`. + """ + + def __init__(self, globs: GlobPatterns) -> None: + self.unfold = OptionalRegexPattern(globs.unfold) + self.delete = OptionalRegexPattern(globs.delete) + self.valid = OptionalRegexPattern(globs.valid) + + self.move = { + key: re.compile(fnmatch.translate(key), re.I) for key in globs.move + } + self.ignore = OptionalRegexPattern(globs.ignore) + + def move_match(self, value: str) -> str | None: + """ + Retrieve the first move patterns that matches the given value, or None if no + move matches. + """ + for key, pattern in self.move.items(): + if pattern.match(value): + return key + return None + + +def _merge_list(l1: list[str] | None, l2: list[str] | None) -> list[str] | None: + if l1 is None and l2 is None: + return None + + return (l1 or []) + (l2 or []) + + +@dataclass(frozen=True, unsafe_hash=True) +class GlobPatterns: + """ + See: `BasicModDataChecker` + """ + + unfold: list[str] | None = None + valid: list[str] | None = None + delete: list[str] | None = None + move: dict[str, str] = field(default_factory=dict[str, str]) + ignore: list[str] | None = None + + def merge( + self, other: GlobPatterns, mode: Literal["merge", "replace"] = "replace" + ) -> GlobPatterns: + """ + Construct a new GlobPatterns by merging the current one with the given one. + + There are two different modes: + - 'merge': In this mode, unfold/valid/delete are concatenated and move + will contain the union of key from self and other, with values from other + overriding common keys. + - 'replace': The merged object will contains attributes from other, except + for None attributes taken from self. + + Args: + other: Other patterns to "merge" with this one. + mode: Merge mode. + + Returns: + A new glob pattern representing the merge of this one with other. + """ + if mode == "merge": + return GlobPatterns( + unfold=_merge_list(self.unfold, other.unfold), + valid=_merge_list(self.valid, other.valid), + delete=_merge_list(self.delete, other.delete), + move=self.move | other.move, + ignore=_merge_list(self.ignore, other.ignore), + ) + else: + return GlobPatterns( + unfold=other.unfold or self.unfold, + valid=other.valid or self.valid, + delete=other.delete or self.delete, + move=other.move or self.move, + ignore=other.ignore or self.ignore, + ) + + +class BasicModDataChecker(mobase.ModDataChecker): + """Game feature that is used to check and fix the content of a data tree + via simple file definitions. + + The file definitions support glob pattern (without subfolders) and are + checked and fixed in definition order of the `file_patterns` dict. + + Args: + file_patterns (optional): A GlobPatterns object, with the following attributes: + ignore: [ "list of files and folders to ignore." ] + # Check result: unchanged + unfold: [ "list of folders to unfold" ], + # (remove and move contents to parent), after being checked and + # fixed recursively. + # Check result: `mobase.ModDataChecker.VALID`. + + valid: [ "list of files and folders in the right path." ], + # Check result: `mobase.ModDataChecker.VALID`. + + delete: [ "list of files/folders to delete." ], + # Check result: `mobase.ModDataChecker.FIXABLE`. + + move: {"Files/folders to move": "target path"} + # If the path ends with `/` or `\\`, the entry will be inserted + # in the corresponding directory instead of replacing it. + # Check result: `mobase.ModDataChecker.FIXABLE`. + + Example: + + BasicModDataChecker( + GlobPatterns( + valid=["valid_folder", "*.ext1"] + move={"*.ext2": "path/to/target_folder/"} + ) + ) + + See Also: + `mobase.IFileTree.move` for the `"move"` target path specs. + """ + + _file_patterns: GlobPatterns + """Private `file_patterns`, updated together with `._regex` and `._move_targets`.""" + + _regex_patterns: RegexPatterns + """The regex patterns derived from the file (glob) patterns.""" + + def __init__(self, file_patterns: GlobPatterns | None = None): + super().__init__() + + self._file_patterns = file_patterns or GlobPatterns() + self._regex_patterns = RegexPatterns(self._file_patterns) + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + status = mobase.ModDataChecker.INVALID + + rp = self._regex_patterns + for entry in filetree: + name = entry.name().casefold() + + if rp.ignore.match(name): + continue + if rp.unfold.match(name): + if is_directory(entry): + status = self.dataLooksValid(entry) + else: + status = mobase.ModDataChecker.INVALID + break + elif rp.valid.match(name): + if status is mobase.ModDataChecker.INVALID: + status = mobase.ModDataChecker.VALID + elif rp.delete.match(name) or rp.move_match(name) is not None: + status = mobase.ModDataChecker.FIXABLE + else: + status = mobase.ModDataChecker.INVALID + break + return status + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + rp = self._regex_patterns + for entry in list(filetree): + name = entry.name() + + if rp.ignore.match(name): + continue + # unfold first - if this match, entry is a directory (checked in + # dataLooksValid) + if rp.unfold.match(name): + assert is_directory(entry) + filetree.merge(entry) + entry.detach() + + elif rp.valid.match(name): + continue + + elif rp.delete.match(name): + entry.detach() + + elif (move_key := rp.move_match(name)) is not None: + target = self._file_patterns.move[move_key] + filetree.move(entry, target) + + return filetree diff --git a/libs/basic_games/basic_features/basic_save_game_info.py b/libs/basic_games/basic_features/basic_save_game_info.py new file mode 100644 index 0000000..436e724 --- /dev/null +++ b/libs/basic_games/basic_features/basic_save_game_info.py @@ -0,0 +1,226 @@ +# -*- encoding: utf-8 -*- + +import sys +from collections.abc import Mapping +from datetime import datetime +from pathlib import Path +from typing import Any, Callable, Self, Sequence + +from PyQt6.QtCore import QDateTime, QLocale, Qt +from PyQt6.QtGui import QImage, QPixmap +from PyQt6.QtWidgets import QFormLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget + +import mobase + + +def format_date(date_time: QDateTime | datetime | str, format_str: str | None = None): + """Default format for date and time in the `BasicGameSaveGameInfoWidget`. + + Args: + date_time: either a `QDateTime`/`datetime` or a string together with + a `format_str`. + format_str (optional): date/time format string (see `QDateTime.fromString`). + + Returns: + Date and time in short locale format. + """ + if isinstance(date_time, str): + date_time = QDateTime.fromString(date_time, format_str) + return QLocale.system().toString(date_time, QLocale.FormatType.ShortFormat) + + +class BasicGameSaveGame(mobase.ISaveGame): + def __init__(self, filepath: Path): + super().__init__() + self._filepath = filepath + + def getFilepath(self) -> str: + return self._filepath.as_posix() + + def getName(self) -> str: + return self._filepath.name + + def getCreationTime(self): + return QDateTime.fromSecsSinceEpoch(int(self._filepath.stat().st_mtime)) + + def getSaveGroupIdentifier(self) -> str: + return "" + + def allFiles(self) -> list[str]: + return [self.getFilepath()] + + +def get_filedate_metadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str]: + """Returns saves file date as the metadata for `BasicGameSaveGameInfoWidget`.""" + return {"File Date:": format_date(save.getCreationTime())} + + +class BasicGameSaveGameInfoWidget(mobase.ISaveGameInfoWidget): + """Save game info widget to display metadata and a preview.""" + + def __init__( + self, + parent: QWidget | None, + get_preview: ( + Callable[[Path], QPixmap | QImage | Path | str | None] | None + ) = lambda p: None, + get_metadata: ( + Callable[[Path, mobase.ISaveGame], Mapping[str, Any] | None] | None + ) = get_filedate_metadata, + max_width: int = 320, + ): + """ + Args: + parent: parent widget + get_preview (optional): `callback(savegame_path)` returning the + saves preview image or the path to it. + get_metadata (optional): `callback(savegame_path, ISaveGame)` returning + the saves metadata. By default the saves file date is shown. + max_width (optional): The maximum widget and (scaled) preview width. + Defaults to 320. + """ + super().__init__(parent) + + def _no_preview(p: Path) -> None: + return None + + self._get_preview = get_preview or _no_preview + self._get_metadata = get_metadata or get_filedate_metadata + self._max_width = max_width or 320 + + layout = QVBoxLayout() + + # Metadata form + self._metadata_widget = QWidget() + self._metadata_widget.setMaximumWidth(self._max_width) + self._metadata_layout = form_layout = QFormLayout(self._metadata_widget) + form_layout.setContentsMargins(0, 0, 0, 0) + form_layout.setVerticalSpacing(2) + layout.addWidget(self._metadata_widget) + self._metadata_widget.hide() # Backwards compatibility (no metadata) + + # Preview (pixmap) + self._label = QLabel() + layout.addWidget(self._label) + self.setLayout(layout) + + self.setWindowFlags( + Qt.WindowType.ToolTip | Qt.WindowType.BypassGraphicsProxyWidget + ) + + def setSave(self, save: mobase.ISaveGame): + save_path = Path(save.getFilepath()) + + # Clear previous + self.hide() + self._label.clear() + while self._metadata_layout.count(): + layoutItem = self._metadata_layout.takeAt(0) + if layoutItem is not None and (w := layoutItem.widget()): + w.deleteLater() + + # Retrieve the pixmap and metadata: + preview = self._get_preview(save_path) + pixmap = None + + # Set the preview pixmap if the preview file exits + if preview is not None: + if isinstance(preview, str): + preview = Path(preview) + if isinstance(preview, Path): + if preview.exists(): + pixmap = QPixmap(str(preview)) + else: + print( + f"Failed to retrieve the preview, file not found: {preview}", + file=sys.stderr, + ) + elif isinstance(preview, QImage): + pixmap = QPixmap.fromImage(preview) + else: + pixmap = preview + if pixmap and not pixmap.isNull(): + # Scale the pixmap and show it: + pixmap = pixmap.scaledToWidth(self._max_width) + self._label.setPixmap(pixmap) + self._label.show() + else: + self._label.hide() + pixmap = None + + # Add metadata, file date by default. + metadata = self._get_metadata(save_path, save) + if metadata: + for key, value in metadata.items(): + self._metadata_layout.addRow(*self._new_form_row(key, str(value))) + self._metadata_widget.show() + self._metadata_widget.setLayout(self._metadata_layout) + self._metadata_widget.adjustSize() + else: + self._metadata_widget.hide() + + if metadata or pixmap: + self.adjustSize() + self.show() + + def _new_form_row(self, label: str = "", field: str = ""): + qLabel = QLabel(text=label) + qLabel.setAlignment(Qt.AlignmentFlag.AlignTop) + qLabel.setStyleSheet("font: italic") + qLabel.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + qField = QLabel(text=field) + qField.setWordWrap(True) + qField.setAlignment(Qt.AlignmentFlag.AlignTop) + qField.setStyleSheet("font: bold") + qField.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) + return qLabel, qField + + def set_maximum_width(self, width: int): + self._max_width = width + self._metadata_widget.setMaximumWidth(width) + + +class BasicGameSaveGameInfo(mobase.SaveGameInfo): + _get_widget: Callable[[QWidget | None], mobase.ISaveGameInfoWidget | None] | None + + def __init__( + self, + get_preview: ( + Callable[[Path], QPixmap | QImage | Path | str | None] | None + ) = None, + get_metadata: ( + Callable[[Path, mobase.ISaveGame], Mapping[str, Any] | None] | None + ) = None, + max_width: int = 0, + ): + """Args from: `BasicGameSaveGameInfoWidget`.""" + super().__init__() + self._get_widget = lambda parent: BasicGameSaveGameInfoWidget( + parent, get_preview, get_metadata, max_width + ) + + @classmethod + def with_widget( + cls, + widget: type[mobase.ISaveGameInfoWidget] | None, + ) -> Self: + """ + + Args: + widget: a custom `ISaveGameInfoWidget` instead of the default + `BasicGameSaveGameInfoWidget`. + """ + self = cls() + self._get_widget = lambda parent: widget(parent) if widget else None + return self + + def getMissingAssets(self, save: mobase.ISaveGame) -> dict[str, Sequence[str]]: + return {} + + def getSaveGameWidget( + self, parent: QWidget | None = None + ) -> mobase.ISaveGameInfoWidget | None: + if self._get_widget: + return self._get_widget(parent) + else: + return None diff --git a/libs/basic_games/basic_features/utils.py b/libs/basic_games/basic_features/utils.py new file mode 100644 index 0000000..b703901 --- /dev/null +++ b/libs/basic_games/basic_features/utils.py @@ -0,0 +1,7 @@ +from typing import TypeGuard + +import mobase + + +def is_directory(entry: mobase.FileTreeEntry) -> TypeGuard[mobase.IFileTree]: + return entry.isDir() diff --git a/libs/basic_games/basic_game.py b/libs/basic_games/basic_game.py new file mode 100644 index 0000000..e0f7ba5 --- /dev/null +++ b/libs/basic_games/basic_game.py @@ -0,0 +1,668 @@ +from __future__ import annotations + +import shutil +import sys +from pathlib import Path +from typing import Callable, Generic, TypeVar + +from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths +from PyQt6.QtGui import QIcon +from PyQt6.QtWidgets import QMessageBox + +import mobase + +from .basic_features.basic_save_game_info import ( + BasicGameSaveGame, + BasicGameSaveGameInfo, +) + + +def replace_variables(value: str, game: BasicGame) -> str: + """Replace special paths in the given value.""" + + if value.find("%DOCUMENTS%") != -1: + value = value.replace( + "%DOCUMENTS%", + QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.DocumentsLocation + ), + ) + if value.find("%USERPROFILE%") != -1: + value = value.replace( + "%USERPROFILE%", + QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.HomeLocation + ), + ) + if value.find("%GAME_DOCUMENTS%") != -1: + value = value.replace( + "%GAME_DOCUMENTS%", game.documentsDirectory().absolutePath() + ) + if value.find("%GAME_PATH%") != -1: + value = value.replace("%GAME_PATH%", game.gameDirectory().absolutePath()) + + return value + + +_T = TypeVar("_T") + + +class BasicGameMapping(Generic[_T]): + # The game: + _game: "BasicGame" + + # Name of the attribute for exposure: + _exposed_name: str + + # Name of the internal method: + _internal_method_name: str + + # Required: + _required: bool + + # Callable returning a default value (if not required): + _default: Callable[["BasicGame"], _T] + + # Function to apply to the value: + _apply_fn: Callable[[_T | str], _T] | None + + def __init__( + self, + game: BasicGame, + exposed_name: str, + internal_method: str, + default: Callable[[BasicGame], _T] | None = None, + apply_fn: Callable[[_T | str], _T] | None = None, + ): + self._game = game + self._exposed_name = exposed_name + self._internal_method_name = internal_method + self._apply_fn = apply_fn + + if hasattr(game, self._exposed_name): + value = getattr(game, self._exposed_name) + + if self._apply_fn is not None: + try: + value = self._apply_fn(value) + except Exception as err: + raise ValueError( + "Basic game plugin from {} has an invalid {} property.".format( + game._fromName, # pyright: ignore[reportPrivateUsage] + self._exposed_name, + ) + ) from err + self._default = lambda game: value # type: ignore + elif default is not None: + self._default = default # type: ignore + elif getattr(game.__class__, self._internal_method_name) is getattr( + BasicGame, self._internal_method_name + ): + raise ValueError( + "Basic game plugin from {} is missing {} property.".format( + game._fromName, # pyright: ignore[reportPrivateUsage] + self._exposed_name, + ) + ) + + def get(self) -> _T: + """Return the value of this mapping.""" + value = self._default(self._game) # type: ignore + + if isinstance(value, str): + return replace_variables(value, self._game) # type: ignore + elif isinstance(value, QDir): + return QDir(replace_variables(value.path(), self._game)) # type: ignore + + # MO2 does not support Path anywhere so we always convert to str: + elif isinstance(value, Path): + return replace_variables(str(value), self._game) # type: ignore + + return value + + +class BasicGameOptionsMapping(BasicGameMapping[list[_T]]): + """ + Represents a game mappings for which multiple options are possible. The game + plugin is responsible to choose the right option depending on the context. + """ + + _index: int + + def __init__( + self, + game: BasicGame, + exposed_name: str, + internal_method: str, + default: Callable[[BasicGame], _T] | None = None, + apply_fn: Callable[[list[_T] | str], list[_T]] | None = None, + ): + super().__init__(game, exposed_name, internal_method, lambda g: [], apply_fn) + self._index = -1 + self._current_default = default + + def set_index(self, index: int): + """ + Set the index of the option to use. + + Args: + index: Index of the option to use. + """ + self._index = index + + def set_value(self, value: _T): + """ + Set the index corresponding of the given value. If the value is not present, + the index is set to -1. + + Args: + value: The value to set the index to. + """ + try: + self._index = self.get().index(value) + except ValueError: + self._index = -1 + + def has_value(self) -> bool: + """ + Check if a value was set for this options mapping. + + Returns: + True if a value was set, False otherwise. + """ + return self._index != -1 + + def current(self) -> _T: + values = self._default(self._game) # type: ignore + + if not values: + return self._current_default(self._game) # type: ignore + + if self._index == -1: + value = values[0] + else: + value = values[self._index] + + if isinstance(value, str): + return replace_variables(value, self._game) # type: ignore + elif isinstance(value, QDir): + return QDir(replace_variables(value.path(), self._game)) # type: ignore + + return value + + +class BasicGameMappings: + name: BasicGameMapping[str] + author: BasicGameMapping[str] + version: BasicGameMapping[mobase.VersionInfo] + description: BasicGameMapping[str] + gameName: BasicGameMapping[str] + gameShortName: BasicGameMapping[str] + gameNexusName: BasicGameMapping[str] + gameThunderstoreName: BasicGameMapping[str] + validShortNames: BasicGameMapping[list[str]] + nexusGameId: BasicGameMapping[int] + binaryName: BasicGameMapping[str] + launcherName: BasicGameMapping[str] + dataDirectory: BasicGameMapping[str] + documentsDirectory: BasicGameMapping[QDir] + iniFiles: BasicGameMapping[list[str]] + savesDirectory: BasicGameMapping[QDir] + savegameExtension: BasicGameMapping[str] + steamAPPId: BasicGameOptionsMapping[str] + gogAPPId: BasicGameOptionsMapping[str] + originManifestIds: BasicGameOptionsMapping[str] + originWatcherExecutables: BasicGameMapping[list[str]] + epicAPPId: BasicGameOptionsMapping[str] + eaDesktopContentId: BasicGameOptionsMapping[str] + supportURL: BasicGameMapping[str] + + @staticmethod + def _default_documents_directory(game: mobase.IPluginGame): + folders = [ + "{}/My Games/{}".format( + QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.DocumentsLocation + ), + game.gameName(), + ), + "{}/{}".format( + QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.DocumentsLocation + ), + game.gameName(), + ), + ] + for folder in folders: + qdir = QDir(folder) + if qdir.exists(): + return qdir + + return QDir() + + # Game mappings: + def __init__(self, game: BasicGame): + self._game = game + + self.name = BasicGameMapping(game, "Name", "name") + self.author = BasicGameMapping(game, "Author", "author") + self.version = BasicGameMapping( + game, + "Version", + "version", + apply_fn=lambda s: mobase.VersionInfo(s) if isinstance(s, str) else s, + ) + self.description = BasicGameMapping( + game, + "Description", + "description", + lambda g: "Adds basic support for game {}.".format(g.gameName()), + ) + self.gameName = BasicGameMapping(game, "GameName", "gameName") + self.gameShortName = BasicGameMapping(game, "GameShortName", "gameShortName") + self.gameNexusName = BasicGameMapping( + game, + "GameNexusName", + "gameNexusName", + default=lambda g: g.gameShortName(), + ) + self.gameThunderstoreName = BasicGameMapping( + game, + "GameThunderstoreName", + "gameThunderstoreName", + default=lambda g: "", + ) + self.validShortNames = BasicGameMapping( + game, + "GameValidShortNames", + "validShortNames", + default=lambda g: [], + apply_fn=lambda value: ( + [c.strip() for c in value.split(",")] # type: ignore + if isinstance(value, str) + else value + ), + ) + self.nexusGameId = BasicGameMapping( + game, "GameNexusId", "nexusGameID", default=lambda g: 0, apply_fn=int + ) + self.binaryName = BasicGameMapping(game, "GameBinary", "binaryName") + self.launcherName = BasicGameMapping( + game, + "GameLauncher", + "getLauncherName", + default=lambda g: "", + ) + self.dataDirectory = BasicGameMapping(game, "GameDataPath", "dataDirectory") + self.documentsDirectory = BasicGameMapping( + game, + "GameDocumentsDirectory", + "documentsDirectory", + apply_fn=lambda s: QDir(s) if isinstance(s, str) else s, + default=BasicGameMappings._default_documents_directory, + ) + self.iniFiles = BasicGameMapping( + game, + "GameIniFiles", + "iniFiles", + lambda g: [], + apply_fn=lambda value: ( + [c.strip() for c in value.split(",")] + if isinstance(value, str) + else value + ), + ) + self.savesDirectory = BasicGameMapping( + game, + "GameSavesDirectory", + "savesDirectory", + apply_fn=lambda s: QDir(s) if isinstance(s, str) else s, + default=lambda g: g.documentsDirectory(), + ) + self.savegameExtension = BasicGameMapping( + game, "GameSaveExtension", "savegameExtension", default=lambda g: "save" + ) + + # Convert Union[int, str, List[Union[int, str]]] to List[str]. + def ids_apply(v: list[int] | list[str] | int | str) -> list[str]: + """ + Convert various types to a list of string. If the given value is already a + list, returns a new list with all values converted to string, otherwise + returns a list with the value convert to a string as its only element. + """ + if isinstance(v, (int, str)): + v = [str(v)] + return [str(x) for x in v] + + self.steamAPPId = BasicGameOptionsMapping( + game, "GameSteamId", "steamAPPId", default=lambda g: "", apply_fn=ids_apply + ) + self.gogAPPId = BasicGameOptionsMapping( + game, "GameGogId", "gogAPPId", default=lambda g: "", apply_fn=ids_apply + ) + self.originManifestIds = BasicGameOptionsMapping( + game, + "GameOriginManifestIds", + "originManifestIds", + default=lambda g: "", + apply_fn=ids_apply, + ) + self.originWatcherExecutables = BasicGameMapping( + game, + "GameOriginWatcherExecutables", + "originWatcherExecutables", + apply_fn=lambda s: [s] if isinstance(s, str) else s, + default=lambda g: [], + ) + self.epicAPPId = BasicGameOptionsMapping( + game, "GameEpicId", "epicAPPId", default=lambda g: "", apply_fn=ids_apply + ) + self.eaDesktopContentId = BasicGameOptionsMapping( + game, + "GameEaDesktopId", + "eaDesktopContentId", + default=lambda g: "", + apply_fn=ids_apply, + ) + self.supportURL = BasicGameMapping( + game, "GameSupportURL", "supportURL", default=lambda g: "" + ) + + +class BasicGame(mobase.IPluginGame): + """This class implements some methods from mobase.IPluginGame + to make it easier to create game plugins without having to implement + all the methods of mobase.IPluginGame.""" + + # List of steam, GOG, origin and Epic games: + steam_games: dict[str, Path] + gog_games: dict[str, Path] + origin_games: dict[str, Path] + epic_games: dict[str, Path] + eadesktop_games: dict[str, Path] + + @staticmethod + def setup(): + from .eadesktop_utils import find_games as find_eadesktop_games + from .epic_utils import find_games as find_epic_games + from .gog_utils import find_games as find_gog_games + from .origin_utils import find_games as find_origin_games + from .steam_utils import find_games as find_steam_games + + errors: list[tuple[str, Exception]] = [] + BasicGame.steam_games = find_steam_games() + BasicGame.gog_games = find_gog_games() + BasicGame.origin_games = find_origin_games() + BasicGame.epic_games = find_epic_games(errors) + BasicGame.eadesktop_games = find_eadesktop_games(errors) + + if errors: + QMessageBox.critical( + None, + "Errors loading game list", + ( + "The following errors occurred while loading the list of available games:\n" + f"\n- {'\n\n- '.join('\n '.join(str(e) for e in messageError) for messageError in errors)}" + ), + ) + + # File containing the plugin: + _fromName: str + + # Organizer obtained in init: + _organizer: mobase.IOrganizer + + # Path to the game, as set by MO2: + _gamePath: str + + def __init__(self): + super(BasicGame, self).__init__() + + if not hasattr(self, "_fromName"): + self._fromName = self.__class__.__name__ + + self._gamePath = "" + + self._mappings: BasicGameMappings = BasicGameMappings(self) + + def _register_feature(self, feature: mobase.GameFeature) -> bool: + return self._organizer.gameFeatures().registerFeature(self, feature, 0, True) + + # Specific to BasicGame: + def is_steam(self) -> bool: + return self._mappings.steamAPPId.has_value() + + def is_gog(self) -> bool: + return self._mappings.gogAPPId.has_value() + + def is_origin(self) -> bool: + return self._mappings.originManifestIds.has_value() + + def is_epic(self) -> bool: + return self._mappings.epicAPPId.has_value() + + def is_eadesktop(self) -> bool: + return self._mappings.eaDesktopContentId.has_value() + + # IPlugin interface: + + def init(self, organizer: mobase.IOrganizer) -> bool: + self._organizer = organizer + + self._register_feature(BasicGameSaveGameInfo()) + + if self._mappings.originWatcherExecutables.get(): + from .origin_utils import OriginWatcher + + self.origin_watcher = OriginWatcher( + self._mappings.originWatcherExecutables.get() + ) + if not self._organizer.onAboutToRun( + lambda appName: self.origin_watcher.spawn_origin_watcher() + ): + print("Failed to register onAboutToRun callback!", file=sys.stderr) + return False + if not self._organizer.onFinishedRun( + lambda appName, result: self.origin_watcher.stop_origin_watcher() + ): + print("Failed to register onFinishedRun callback!", file=sys.stderr) + return False + return True + + def name(self) -> str: + return self._mappings.name.get() + + def author(self) -> str: + return self._mappings.author.get() + + def description(self) -> str: + return self._mappings.description.get() + + def version(self) -> mobase.VersionInfo: + return self._mappings.version.get() + + def isActive(self) -> bool: + if not self._organizer.managedGame(): + return False + + # Note: self is self._organizer.managedGame() does not work: + return self.name() == self._organizer.managedGame().name() + + def settings(self) -> list[mobase.PluginSetting]: + return [] + + # IPluginGame interface: + + def detectGame(self): + for steam_id in self._mappings.steamAPPId.get(): + if steam_id in BasicGame.steam_games: + self.setGamePath(BasicGame.steam_games[steam_id]) + return + + for gog_id in self._mappings.gogAPPId.get(): + if gog_id in BasicGame.gog_games: + self.setGamePath(BasicGame.gog_games[gog_id]) + return + + for origin_manifest_id in self._mappings.originManifestIds.get(): + if origin_manifest_id in BasicGame.origin_games: + self.setGamePath(BasicGame.origin_games[origin_manifest_id]) + return + + for epic_id in self._mappings.epicAPPId.get(): + if epic_id in BasicGame.epic_games: + self.setGamePath(BasicGame.epic_games[epic_id]) + return + + for eadesktop_content_id in self._mappings.eaDesktopContentId.get(): + if eadesktop_content_id in BasicGame.eadesktop_games: + self.setGamePath(BasicGame.eadesktop_games[eadesktop_content_id]) + return + + def gameName(self) -> str: + return self._mappings.gameName.get() + + def gameShortName(self) -> str: + return self._mappings.gameShortName.get() + + def gameIcon(self) -> QIcon: + return mobase.getIconForExecutable( + self.gameDirectory().absoluteFilePath(self.binaryName()) + ) + + def validShortNames(self) -> list[str]: + return self._mappings.validShortNames.get() + + def gameNexusName(self) -> str: + return self._mappings.gameNexusName.get() + + def gameThunderstoreName(self) -> str: + return self._mappings.gameThunderstoreName.get() + + def nexusModOrganizerID(self) -> int: + return 0 + + def nexusGameID(self) -> int: + return self._mappings.nexusGameId.get() + + def steamAPPId(self) -> str: + return self._mappings.steamAPPId.current() + + def gogAPPId(self) -> str: + return self._mappings.gogAPPId.current() + + def epicAPPId(self) -> str: + return self._mappings.epicAPPId.current() + + def eaDesktopContentId(self) -> str: + return self._mappings.eaDesktopContentId.current() + + def binaryName(self) -> str: + return self._mappings.binaryName.get() + + def getLauncherName(self) -> str: + return self._mappings.launcherName.get() + + def getSupportURL(self) -> str: + return self._mappings.supportURL.get() + + def iniFiles(self) -> list[str]: + return self._mappings.iniFiles.get() + + def executables(self) -> list[mobase.ExecutableInfo]: + execs: list[mobase.ExecutableInfo] = [] + if self.getLauncherName(): + execs.append( + mobase.ExecutableInfo( + self.gameName(), + QFileInfo( + self.gameDirectory().absoluteFilePath(self.getLauncherName()) + ), + ) + ) + execs.append( + mobase.ExecutableInfo( + self.gameName(), + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ) + return execs + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + return [] + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + BasicGameSaveGame(path) + for path in Path(folder.absolutePath()).glob(f"**/*.{ext}") + ] + + def initializeProfile( + self, directory: QDir, settings: mobase.ProfileSetting + ) -> None: + if settings & mobase.ProfileSetting.CONFIGURATION: + for iniFile in self.iniFiles(): + try: + shutil.copyfile( + self.documentsDirectory().absoluteFilePath(iniFile), + directory.absoluteFilePath(QFileInfo(iniFile).fileName()), + ) + except FileNotFoundError: + Path( + directory.absoluteFilePath(QFileInfo(iniFile).fileName()) + ).touch() + + def setGameVariant(self, variant: str) -> None: + pass + + def gameVersion(self) -> str: + return mobase.getFileVersion( + self.gameDirectory().absoluteFilePath(self.binaryName()) + ) + + def looksValid(self, directory: QDir): + return directory.exists(self.binaryName()) + + def isInstalled(self) -> bool: + return bool(self._gamePath) + + def gameDirectory(self) -> QDir: + """ + @return directory (QDir) to the game installation. + """ + return QDir(self._gamePath) + + def dataDirectory(self) -> QDir: + return QDir( + self.gameDirectory().absoluteFilePath(self._mappings.dataDirectory.get()) + ) + + def setGamePath(self, path: Path | str) -> None: + self._gamePath = str(path) + + path = Path(path) + + # Check if we have a matching steam, GOG, Origin or EA Desktop id and set the + # index accordingly: + for steamid, steampath in BasicGame.steam_games.items(): + if steampath == path: + self._mappings.steamAPPId.set_value(steamid) + for gogid, gogpath in BasicGame.gog_games.items(): + if gogpath == path: + self._mappings.gogAPPId.set_value(gogid) + for originid, originpath in BasicGame.origin_games.items(): + if originpath == path: + self._mappings.originManifestIds.set_value(originid) + for epicid, epicpath in BasicGame.epic_games.items(): + if epicpath == path: + self._mappings.epicAPPId.set_value(epicid) + for eadesktopid, eadesktoppath in BasicGame.eadesktop_games.items(): + if eadesktoppath == path: + self._mappings.eaDesktopContentId.set_value(eadesktopid) + + def documentsDirectory(self) -> QDir: + return self._mappings.documentsDirectory.get() + + def savesDirectory(self) -> QDir: + return self._mappings.savesDirectory.get() diff --git a/libs/basic_games/basic_game_ini.py b/libs/basic_games/basic_game_ini.py new file mode 100644 index 0000000..c9e0af6 --- /dev/null +++ b/libs/basic_games/basic_game_ini.py @@ -0,0 +1,23 @@ +# -*- encoding: utf-8 -*- + +import configparser +import os + +from .basic_game import BasicGame + + +class BasicIniGame(BasicGame): + def __init__(self, path: str): + # Set the _fromName to get more "correct" errors: + self._fromName = os.path.basename(path) + + # Read the file: + config = configparser.ConfigParser() + config.optionxform = str # type: ignore + config.read(path) + + # Just fill the class with values: + for k, v in config["DEFAULT"].items(): + setattr(self, k, v) + + super().__init__() diff --git a/libs/basic_games/eadesktop_utils.py b/libs/basic_games/eadesktop_utils.py new file mode 100644 index 0000000..5720556 --- /dev/null +++ b/libs/basic_games/eadesktop_utils.py @@ -0,0 +1,85 @@ +# -*- encoding: utf-8 -*- + +import configparser +import os +import sys +import xml.etree.ElementTree as et +from configparser import NoOptionError +from pathlib import Path +from typing import Dict + + +def find_games(errors: list[tuple[str, Exception]] | None = None) -> Dict[str, Path]: + """ + Find the list of EA Desktop games installed. + + Returns: + A mapping from EA Desktop content IDs to install locations for available + EA Desktop games. + """ + games: Dict[str, Path] = {} + + local_app_data_path = os.path.expandvars("%LocalAppData%") + ea_desktop_settings_path = Path(local_app_data_path).joinpath( + "Electronic Arts", "EA Desktop" + ) + + if not ea_desktop_settings_path.exists(): + return games + + try: + user_ini, *_ = list(ea_desktop_settings_path.glob("user_*.ini")) + except ValueError: + return games + + # The INI file in its current form has no section headers. + # So we wrangle the input to add it all under a fake section. + with open(user_ini) as f: + ini_content = "[mod_organizer]\n" + f.read() + + config = configparser.ConfigParser() + try: + config.read_string(ini_content) + except configparser.ParsingError as e: + error_message = ( + f'Failed to parse EA Desktop games list file "{user_ini}",\n' + " Try to run the launcher to recreate it." + ) + print(error_message, e, file=sys.stderr) + if errors is not None: + errors.append((error_message, e)) + return games + + try: + install_path = Path(config.get("mod_organizer", "user.downloadinplacedir")) + except NoOptionError: + install_path = Path(os.environ["ProgramW6432"]) / "EA Games" + config.set("mod_organizer", "user.downloadinplacedir", install_path.__str__()) + + if not install_path.exists(): + return games + + for game_dir in install_path.iterdir(): + try: + installer_file = game_dir.joinpath("__Installer", "installerdata.xml") + xml_tree = et.parse(installer_file) + root = xml_tree.getroot() + + # For all manifest files the following XPath expression returns the + # numeric ID. There are, in some cases, also name IDs but we do not + # consider these. + content_id = root.find(".//contentIDs/contentID[1]") + + if content_id is not None and content_id.text: + game_id = content_id.text + games[game_id] = game_dir + except FileNotFoundError: + pass + + return games + + +if __name__ == "__main__": + games = find_games() + for k, v in games.items(): + print("Found game with id {} at {}.".format(k, v)) diff --git a/libs/basic_games/epic_utils.py b/libs/basic_games/epic_utils.py new file mode 100644 index 0000000..94ae6a9 --- /dev/null +++ b/libs/basic_games/epic_utils.py @@ -0,0 +1,108 @@ +# -*- encoding: utf-8 -*- +from __future__ import annotations + +import itertools +import json +import os +import sys +from collections.abc import Iterable +from pathlib import Path + +try: + import winreg +except ImportError: + winreg = None + +ErrorList = list[tuple[str, Exception]] + + +def find_epic_games( + errors: ErrorList | None = None, +) -> Iterable[tuple[str, Path]]: + if winreg is None: + return + + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + r"Software\Wow6432Node\Epic Games\EpicGamesLauncher", + ) as key: + epic_data_path, _ = winreg.QueryValueEx(key, "AppDataPath") + except FileNotFoundError: + epic_data_path = r"%ProgramData%\Epic\EpicGamesLauncher\Data" + + manifests_path = Path(os.path.expandvars(epic_data_path)).joinpath("Manifests") + if manifests_path.exists(): + for manifest_file_path in manifests_path.glob("*.item"): + try: + with open(manifest_file_path, encoding="utf-8") as manifest_file: + manifest_file_data = json.load(manifest_file) + yield ( + manifest_file_data["AppName"], + Path(manifest_file_data["InstallLocation"]), + ) + except (json.JSONDecodeError, KeyError) as e: + error_message = ( + f'Unable to parse Epic Games manifest file: "{manifest_file_path}"\n' + " Try to run the launcher recreate it." + ) + print( + error_message, + e, + file=sys.stderr, + ) + if errors is not None: + errors.append((error_message, e)) + + +def find_legendary_games( + config_path: str | None = None, errors: ErrorList | None = None +) -> Iterable[tuple[str, Path]]: + # Based on legendary source: + # https://github.com/derrod/legendary/blob/master/legendary/lfs/lgndry.py + if config_path := config_path or os.environ.get("XDG_CONFIG_HOME"): + legendary_config_path = Path(config_path, "legendary") + else: + legendary_config_path = Path("~/.config/legendary").expanduser() + + installed_path = legendary_config_path / "installed.json" + if installed_path.exists(): + try: + with open(installed_path, encoding="utf-8") as installed_file: + installed_games = json.load(installed_file) + for game in installed_games.values(): + yield game["app_name"], Path(game["install_path"]) + except (json.JSONDecodeError, AttributeError, KeyError) as e: + error_message = ( + f'Unable to parse installed games from Legendary/Heroic launcher: "{installed_path}"\n' + " Try to run the launcher to recrated the file." + ) + print( + error_message, + e, + file=sys.stderr, + ) + if errors is not None: + errors.append((error_message, e)) + + +def find_heroic_games(errors: ErrorList | None = None): + return find_legendary_games( + os.path.expandvars(r"%AppData%\heroic\legendaryConfig"), errors + ) + + +def find_games(errors: ErrorList | None = None) -> dict[str, Path]: + return dict( + itertools.chain( + find_epic_games(errors=errors), + find_legendary_games(errors=errors), + find_heroic_games(errors=errors), + ) + ) + + +if __name__ == "__main__": + games = find_games() + for k, v in games.items(): + print("Found game with id {} at {}.".format(k, v)) diff --git a/libs/basic_games/games/__init__.py b/libs/basic_games/games/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/basic_games/games/baldursgate3/__init__.py b/libs/basic_games/games/baldursgate3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/basic_games/games/baldursgate3/bg3_data_checker.py b/libs/basic_games/games/baldursgate3/bg3_data_checker.py new file mode 100644 index 0000000..695d21c --- /dev/null +++ b/libs/basic_games/games/baldursgate3/bg3_data_checker.py @@ -0,0 +1,58 @@ +from pathlib import Path + +import mobase + +from ...basic_features import BasicModDataChecker, GlobPatterns, utils +from . import bg3_utils + + +class BG3ModDataChecker(BasicModDataChecker): + def __init__(self): + super().__init__( + GlobPatterns( + valid=[ + "*.pak", + str(Path("Mods") / "*.pak"), # standard mods + "bin", # native mods / Script Extender + "Script Extender", # mods which are configured via jsons in this folder + "Data", # loose file mods + ] + + [str(Path("*") / f) for f in bg3_utils.loose_file_folders], + move={ + "Root/": "", # root builder not needed + "*.dll": "bin/", + "ScriptExtenderSettings.json": "bin/", + } + | {f: "Data/" for f in bg3_utils.loose_file_folders}, + delete=["info.json", "*.txt"], + ) + ) + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + status = mobase.ModDataChecker.INVALID + rp = self._regex_patterns + for entry in filetree: + name = entry.name().casefold() + if rp.unfold.match(name): + if utils.is_directory(entry): + status = self.dataLooksValid(entry) + else: + status = mobase.ModDataChecker.INVALID + break + elif rp.valid.match(name): + if status is mobase.ModDataChecker.INVALID: + status = mobase.ModDataChecker.VALID + elif isinstance(entry, mobase.IFileTree): + status = ( + mobase.ModDataChecker.VALID + if all(rp.valid.match(e.pathFrom(filetree)) for e in entry) + else mobase.ModDataChecker.INVALID + ) + elif rp.delete.match(name) or rp.move_match(name) is not None: + status = mobase.ModDataChecker.FIXABLE + else: + status = mobase.ModDataChecker.INVALID + break + return status diff --git a/libs/basic_games/games/baldursgate3/bg3_data_content.py b/libs/basic_games/games/baldursgate3/bg3_data_content.py new file mode 100644 index 0000000..c378520 --- /dev/null +++ b/libs/basic_games/games/baldursgate3/bg3_data_content.py @@ -0,0 +1,54 @@ +from enum import IntEnum, auto + +import mobase + +from . import bg3_utils + + +class Content(IntEnum): + PAK = auto() + WORKSPACE = auto() + NATIVE = auto() + LOOSE_FILES = auto() + SE_FILES = auto() + + +class BG3DataContent(mobase.ModDataContent): + BG3_CONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.WORKSPACE, "Mod workspace", ":/MO/gui/content/script"), + (Content.PAK, "Pak", ":/MO/gui/content/bsa"), + (Content.LOOSE_FILES, "Loose file override mod", ":/MO/gui/content/texture"), + (Content.SE_FILES, "Script Extender Files", ":/MO/gui/content/inifile"), + (Content.NATIVE, "Native DLL mod", ":/MO/gui/content/plugin"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.BG3_CONTENTS + ] + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + contents: set[int] = set() + for entry in filetree: + if isinstance(entry, mobase.IFileTree): + match entry.name(): + case "Script Extender": + contents.add(Content.SE_FILES) + case "Data": + contents.add(Content.LOOSE_FILES) + case "Mods": + for e in entry: + if e.name().endswith(".pak"): + contents.add(Content.PAK) + break + case "bin": + contents.add(Content.NATIVE) + case _: + for e in entry: + if e.name() in bg3_utils.loose_file_folders: + contents.add(Content.WORKSPACE) + break + elif entry.name().endswith(".pak"): + contents.add(Content.PAK) + return list(contents) diff --git a/libs/basic_games/games/baldursgate3/bg3_file_mapper.py b/libs/basic_games/games/baldursgate3/bg3_file_mapper.py new file mode 100644 index 0000000..0ae6ef8 --- /dev/null +++ b/libs/basic_games/games/baldursgate3/bg3_file_mapper.py @@ -0,0 +1,131 @@ +import functools +import os +from pathlib import Path +from typing import Callable, Optional + +from PyQt6.QtCore import QDir, QLoggingCategory, qDebug, qInfo, qWarning +from PyQt6.QtWidgets import QApplication + +import mobase + +from . import bg3_utils + + +class BG3FileMapper(mobase.IPluginFileMapper): + current_mappings: list[mobase.Mapping] = [] + + def __init__(self, utils: bg3_utils.BG3Utils, doc_dir: Callable[[], QDir]): + super().__init__() + self._utils = utils + self.doc_dir = doc_dir + + @functools.cached_property + def doc_path(self): + return Path(self.doc_dir().path()) + + def mappings(self) -> list[mobase.Mapping]: + qInfo("creating custom bg3 mappings") + self.current_mappings.clear() + active_mods = self._utils.active_mods() + if not active_mods: + return [] + progress = self._utils.create_progress_window( + "Mapping files to documents folder", len(active_mods) + 1 + ) + docs_path_mods = self.doc_path / "Mods" + docs_path_se = self.doc_path / "Script Extender" + for mod in active_mods: + modpath = Path(mod.absolutePath()) + self.map_files(modpath, dest=docs_path_mods, pattern="*.pak", rel=False) + self.map_files(modpath / "Script Extender", dest=docs_path_se) + if self._utils.convert_yamls_to_json: + self.map_files(modpath / "bin", only_convert=True) + progress.setValue(progress.value() + 1) + QApplication.processEvents() + if progress.wasCanceled(): + qWarning("mapping canceled by user") + return self.current_mappings + (self._utils.overwrite_path / "Script Extender").mkdir( + parents=True, exist_ok=True + ) + (self._utils.overwrite_path / "Stats").mkdir(parents=True, exist_ok=True) + (self._utils.overwrite_path / "Temp").mkdir(parents=True, exist_ok=True) + (self._utils.overwrite_path / "LevelCache").mkdir(parents=True, exist_ok=True) + (self._utils.overwrite_path / "Stats").mkdir(parents=True, exist_ok=True) + (self._utils.overwrite_path / "Mods").mkdir(parents=True, exist_ok=True) + (self._utils.overwrite_path / "GMCampaigns").mkdir(parents=True, exist_ok=True) + self.map_files(self._utils.overwrite_path) + self.create_mapping( + self._utils.modsettings_path, + self.doc_path + / "PlayerProfiles" + / "Public" + / self._utils.modsettings_path.name, + ) + progress.setValue(len(active_mods) + 1) + QApplication.processEvents() + progress.close() + cat = QLoggingCategory.defaultCategory() + if cat is not None and cat.isDebugEnabled(): + qDebug( + f"resolved mappings: { {m.source: m.destination for m in self.current_mappings} }" + ) + return self.current_mappings + + def map_files( + self, + path: Path, + dest: Optional[Path] = None, + pattern: str = "*", + rel: bool = True, + only_convert: bool = False, + ): + dest = dest if dest else self.doc_path + dest_func: Callable[[Path], str] = ( + (lambda f: os.path.relpath(f, path)) if rel else lambda f: f.name + ) + found_jsons: set[Path] = set() + for file in list(path.rglob(pattern)): + if self._utils.convert_yamls_to_json and ( + file.name.endswith(".yaml") or file.name.endswith(".yml") + ): + converted_path = file.parent / file.name.replace( + ".yaml", ".json" + ).replace(".yml", ".json") + try: + if not converted_path.exists() or os.path.getmtime( + file + ) > os.path.getmtime(converted_path): + import json + + import yaml + + with open(file, "r") as yaml_file: + with open(converted_path, "w") as json_file: + json.dump( + yaml.safe_load(yaml_file), json_file, indent=2 + ) + qDebug(f"Converted {file} to JSON") + found_jsons.add(converted_path) + except OSError as e: + qWarning(f"Error accessing file {converted_path}: {e}") + elif file.name.endswith(".json"): + found_jsons.add(file) + elif not only_convert: + self.create_mapping(file, dest / dest_func(file)) + if only_convert: + return + for file in found_jsons: + self.create_mapping(file, dest / dest_func(file)) + + def create_mapping(self, file: Path, dest: Path): + bg3_utils.create_dir_if_needed(dest) + + self.current_mappings.append( + mobase.Mapping( + source=str(file), + destination=str(dest), + is_directory=file.is_dir(), + create_target=True, + ) + ) diff --git a/libs/basic_games/games/baldursgate3/bg3_utils.py b/libs/basic_games/games/baldursgate3/bg3_utils.py new file mode 100644 index 0000000..958d173 --- /dev/null +++ b/libs/basic_games/games/baldursgate3/bg3_utils.py @@ -0,0 +1,274 @@ +import functools +import shutil +import typing +from pathlib import Path +from time import sleep + +from PyQt6.QtCore import ( + QCoreApplication, + QDir, + QEventLoop, + QRunnable, + Qt, + QThread, + QThreadPool, + qInfo, + qWarning, +) +from PyQt6.QtWidgets import QApplication, QMainWindow, QProgressDialog + +import mobase + +loose_file_folders = { + "Public", + "Mods", + "Generated", + "Localization", + "ScriptExtender", +} + + +def get_node_string( + folder: str = "", + md5: str = "", + name: str = "", + publish_handle: str = "0", + uuid: str = "", + version64: str = "0", +) -> str: + return f""" + + + + + + + + """ + + +class BG3Utils: + _mod_settings_xml_start = """\ + + + + + + + + """ + get_node_string( + folder="GustavX", + name="GustavX", + uuid="cb555efe-2d9e-131f-8195-a89329d218ea", + version64="36028797018963968", + ) + _mod_settings_xml_end = """ + + + + + +""" + + def __init__(self, name: str): + self.main_window = None + self._name = name + from . import lslib_retriever, pak_parser + + self.lslib_retriever = lslib_retriever.LSLibRetriever(self) + self._pak_parser = pak_parser.BG3PakParser(self) + + def init(self, organizer: mobase.IOrganizer): + self._organizer = organizer + + @functools.cached_property + def autobuild_paks(self): + return bool(self.get_setting("autobuild_paks")) + + @functools.cached_property + def extract_full_package(self): + return bool(self.get_setting("extract_full_package")) + + @functools.cached_property + def remove_extracted_metadata(self): + return bool(self.get_setting("remove_extracted_metadata")) + + @functools.cached_property + def force_load_dlls(self): + return bool(self.get_setting("force_load_dlls")) + + @functools.cached_property + def log_diff(self): + return bool(self.get_setting("log_diff")) + + @functools.cached_property + def convert_yamls_to_json(self): + return bool(self.get_setting("convert_yamls_to_json")) + + @functools.cached_property + def log_dir(self): + return create_dir_if_needed(Path(self._organizer.basePath()) / "logs") + + @functools.cached_property + def modsettings_backup(self): + return create_dir_if_needed(self.plugin_data_path / "temp" / "modsettings.lsx") + + @functools.cached_property + def modsettings_path(self): + return create_dir_if_needed( + Path(self._organizer.profilePath()) / "modsettings.lsx" + ) + + @functools.cached_property + def plugin_data_path(self) -> Path: + """Gets the path to the data folder for the current plugin.""" + return create_dir_if_needed( + Path(self._organizer.pluginDataPath(), self._name).absolute() + ) + + @functools.cached_property + def tools_dir(self): + return create_dir_if_needed(self.plugin_data_path / "tools") + + @functools.cached_property + def overwrite_path(self): + return create_dir_if_needed(Path(self._organizer.overwritePath())) + + def active_mods(self) -> list[mobase.IModInterface]: + modlist = self._organizer.modList() + return [ + modlist.getMod(mod_name) + for mod_name in filter( + lambda mod: modlist.state(mod) & mobase.ModState.ACTIVE, + modlist.allModsByProfilePriority(), + ) + ] + + def _set_setting(self, key: str, value: mobase.MoVariant): + self._organizer.setPluginSetting(self._name, key, value) + + def get_setting(self, key: str) -> mobase.MoVariant: + return self._organizer.pluginSetting(self._name, key) + + def tr(self, trstr: str) -> str: + return QCoreApplication.translate(self._name, trstr) + + def create_progress_window( + self, title: str, max_progress: int, msg: str = "", cancelable: bool = True + ) -> QProgressDialog: + progress = QProgressDialog( + self.tr(msg if msg else title), + self.tr("Cancel") if cancelable else None, + 0, + max_progress, + self.main_window, + ) + progress.setWindowTitle(self.tr(f"BG3 Plugin: {title}")) + progress.setWindowModality(Qt.WindowModality.ApplicationModal) + progress.show() + return progress + + def on_user_interface_initialized(self, window: QMainWindow) -> None: + self.main_window = window + + def on_settings_changed( + self, + plugin_name: str, + setting: str, + old: mobase.MoVariant, + new: mobase.MoVariant, + ) -> None: + if self._name != plugin_name: + return + if setting in { + "extract_full_package", + "autobuild_paks", + "remove_extracted_metadata", + "force_load_dlls", + "log_diff", + "convert_yamls_to_json", + } and hasattr(self, setting): + delattr(self, setting) + + def construct_modsettings_xml( + self, + exec_path: str = "", + working_dir: typing.Optional[QDir] = None, + args: str = "", + force_reparse_metadata: bool = False, + ) -> bool: + if ( + "bin/bg3" not in exec_path + or not self.lslib_retriever.download_lslib_if_missing() + ): + return True + active_mods = self.active_mods() + progress = self.create_progress_window( + "Generating modsettings.xml", len(active_mods) + ) + threadpool = QThreadPool.globalInstance() + if threadpool is None: + return False + metadata: dict[str, str] = {} + + def retrieve_mod_metadata_in_new_thread(mod: mobase.IModInterface): + return lambda: metadata.update( + self._pak_parser.get_metadata_for_files_in_mod( + mod, force_reparse_metadata + ) + ) + + for mod in active_mods: + if progress.wasCanceled(): + qWarning("processing canceled by user") + return False + threadpool.start(QRunnable.create(retrieve_mod_metadata_in_new_thread(mod))) + count = 0 + num_active_mods = len(active_mods) + total_intervals_to_wait = (num_active_mods * 2) + 20 + while len(metadata.keys()) < num_active_mods: + progress.setValue(len(metadata.keys())) + QApplication.processEvents(QEventLoop.ProcessEventsFlag.AllEvents, 100) + count += 1 + if count == total_intervals_to_wait or progress.wasCanceled(): + remaining_mods = {mod.name() for mod in active_mods} - metadata.keys() + qWarning(f"processing did not finish in time for: {remaining_mods}") + progress.close() + break + QThread.msleep(100) + progress.setValue(num_active_mods) + QApplication.processEvents(QEventLoop.ProcessEventsFlag.AllEvents, 100) + progress.close() + qInfo(f"writing mod load order to {self.modsettings_path}") + self.modsettings_path.parent.mkdir(parents=True, exist_ok=True) + self.modsettings_path.write_text( + ( + self._mod_settings_xml_start + + "".join( + metadata[mod.name()] + for mod in active_mods + if mod.name() in metadata + ) + + self._mod_settings_xml_end + ) + ) + qInfo( + f"backing up generated file {self.modsettings_path} to {self.modsettings_backup}, " + f"check the backup after the executable runs for differences with the file used by the game if you encounter issues" + ) + self.modsettings_backup.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(self.modsettings_path, self.modsettings_backup) + sleep(0.5) + return True + + def on_mod_installed(self, mod: mobase.IModInterface) -> None: + if self.lslib_retriever.download_lslib_if_missing(): + self._pak_parser.get_metadata_for_files_in_mod(mod, True) + + +def create_dir_if_needed(path: Path) -> Path: + if "." not in path.name[1:]: + path.mkdir(parents=True, exist_ok=True) + else: + path.parent.mkdir(parents=True, exist_ok=True) + return path diff --git a/libs/basic_games/games/baldursgate3/lslib_retriever.py b/libs/basic_games/games/baldursgate3/lslib_retriever.py new file mode 100644 index 0000000..ee1122c --- /dev/null +++ b/libs/basic_games/games/baldursgate3/lslib_retriever.py @@ -0,0 +1,162 @@ +import json +import shutil +import traceback +import urllib.request +import zipfile +from functools import cached_property + +from PyQt6.QtCore import qDebug, qWarning +from PyQt6.QtWidgets import QApplication, QMessageBox + +from . import bg3_utils + + +class LSLibRetriever: + def __init__(self, utils: bg3_utils.BG3Utils): + self._utils = utils + + @cached_property + def _needed_lslib_files(self): + return { + self._utils.tools_dir / x + for x in { + "CommandLineArgumentsParser.dll", + "Divine.dll", + "Divine.dll.config", + "Divine.exe", + "Divine.runtimeconfig.json", + "K4os.Compression.LZ4.dll", + "K4os.Compression.LZ4.Streams.dll", + "LSLib.dll", + "LSLibNative.dll", + "LZ4.dll", + "Newtonsoft.Json.dll", + "System.IO.Hashing.dll", + "ZstdSharp.dll", + } + } + + def download_lslib_if_missing(self, force: bool = False) -> bool: + if not force and all(x.exists() for x in self._needed_lslib_files): + return True + try: + self._utils.tools_dir.mkdir(exist_ok=True, parents=True) + downloaded = False + + def reporthook(block_num: int, block_size: int, total_size: int) -> None: + if total_size > 0: + progress.setValue( + min(int(block_num * block_size * 100 / total_size), 100) + ) + QApplication.processEvents() + + with urllib.request.urlopen( + "https://api.github.com/repos/Norbyte/lslib/releases/latest" + ) as response: + assets = json.loads(response.read().decode("utf-8"))["assets"][0] + zip_path = self._utils.tools_dir / assets["name"] + if not zip_path.exists(): + old_archives = list(self._utils.tools_dir.glob("*.zip")) + msg_box = QMessageBox(self._utils.main_window) + msg_box.setWindowTitle( + self._utils.tr("Baldur's Gate 3 Plugin - Missing dependencies") + ) + if old_archives: + msg_box.setText(self._utils.tr("LSLib update available.")) + else: + msg_box.setText( + self._utils.tr( + "LSLib tools are missing.\nThese are necessary for the plugin to create the load order file for BG3." + ) + ) + msg_box.addButton( + self._utils.tr("Download"), + QMessageBox.ButtonRole.DestructiveRole, + ) + exit_btn = msg_box.addButton( + self._utils.tr("Exit"), QMessageBox.ButtonRole.ActionRole + ) + msg_box.setIcon(QMessageBox.Icon.Warning) + msg_box.exec() + + if msg_box.clickedButton() == exit_btn: + if not old_archives: + err = QMessageBox(self._utils.main_window) + err.setIcon(QMessageBox.Icon.Critical) + err.setText( + "LSLib tools are required for the proper generation of the modsettings.xml file, file will not be generated" + ) + err.exec() + return False + else: + progress = self._utils.create_progress_window( + "Downloading LSLib", 100, cancelable=False + ) + urllib.request.urlretrieve( + assets["browser_download_url"], str(zip_path), reporthook + ) + progress.close() + downloaded = True + for archive in old_archives: + archive.unlink() + old_archives = [] + else: + old_archives = [] + new_msg = QMessageBox(self._utils.main_window) + new_msg.setIcon(QMessageBox.Icon.Information) + new_msg.setText( + self._utils.tr("Latest version of LSLib already downloaded!") + ) + new_msg.exec() + + except Exception as e: + qDebug(f"Download failed: {e}") + err = QMessageBox(self._utils.main_window) + err.setIcon(QMessageBox.Icon.Critical) + err.setText( + self._utils.tr( + f"Failed to download LSLib tools:\n{traceback.format_exc()}" + ) + ) + err.exec() + return False + try: + if old_archives: + zip_path = sorted(old_archives)[-1] + if old_archives or not downloaded: + dialog_message = "Ensuring all necessary LSLib files have been extracted from archive..." + win_title = "Verifying LSLib files" + else: + dialog_message = "Extracting/Updating LSLib files..." + win_title = "Extracting LSLib" + x_progress = self._utils.create_progress_window( + win_title, len(self._needed_lslib_files), msg=dialog_message + ) + with zipfile.ZipFile(zip_path, "r") as zip_ref: + for file in self._needed_lslib_files: + if downloaded or not file.exists(): + shutil.move( + zip_ref.extract( + f"Packed/Tools/{file.name}", self._utils.tools_dir + ), + file, + ) + x_progress.setValue(x_progress.value() + 1) + QApplication.processEvents() + if x_progress.wasCanceled(): + qWarning("processing canceled by user") + return False + x_progress.close() + shutil.rmtree(self._utils.tools_dir / "Packed", ignore_errors=True) + except Exception as e: + qDebug(f"Extraction failed: {e}") + err = QMessageBox(self._utils.main_window) + err.setIcon(QMessageBox.Icon.Critical) + err.setText( + self._utils.tr( + f"Failed to extract LSLib tools:\n{traceback.format_exc()}" + ) + ) + err.exec() + return False + return True diff --git a/libs/basic_games/games/baldursgate3/pak_parser.py b/libs/basic_games/games/baldursgate3/pak_parser.py new file mode 100644 index 0000000..18598a9 --- /dev/null +++ b/libs/basic_games/games/baldursgate3/pak_parser.py @@ -0,0 +1,295 @@ +import configparser +import hashlib +import os +import re +import shutil +import subprocess +import traceback +from functools import cached_property +from pathlib import Path +from typing import Callable +from xml.etree import ElementTree +from xml.etree.ElementTree import Element + +from PyQt6.QtCore import ( + qDebug, + qInfo, + qWarning, +) + +import mobase + +from . import bg3_utils + + +class BG3PakParser: + def __init__(self, utils: bg3_utils.BG3Utils): + self._utils = utils + + _mod_cache: dict[Path, bool] = {} + _types = { + "Folder": "", + "MD5": "", + "Name": "", + "PublishHandle": "0", + "UUID": "", + "Version64": "0", + } + + @cached_property + def _divine_command(self): + return f"{self._utils.tools_dir / 'Divine.exe'} -g bg3 -l info" + + @cached_property + def _folder_pattern(self): + return re.compile("Data|Script Extender|bin|Mods") + + def get_metadata_for_files_in_mod( + self, mod: mobase.IModInterface, force_reparse_metadata: bool + ): + return { + mod.name(): "".join( + [ + self._get_metadata_for_file(mod, file, force_reparse_metadata) + for file in sorted( + list(Path(mod.absolutePath()).rglob("*.pak")) + + ( + [ + f + for f in Path(mod.absolutePath()).glob("*") + if f.is_dir() + ] + if self._utils.autobuild_paks + else [] + ) + ) + ] + ) + } + + def _get_metadata_for_file( + self, + mod: mobase.IModInterface, + file: Path, + force_reparse_metadata: bool, + ) -> str: + meta_ini = Path(mod.absolutePath()) / "meta.ini" + config = configparser.ConfigParser(interpolation=None) + config.read(meta_ini, encoding="utf-8") + try: + if file.name.endswith("pak"): + meta_file = ( + self._utils.plugin_data_path + / "temp" + / "extracted_metadata" + / f"{file.name[: int(len(file.name) / 2)]}-{hashlib.md5(str(file).encode(), usedforsecurity=False).hexdigest()[:5]}.lsx" + ) + try: + if ( + not force_reparse_metadata + and config.has_section(file.name) + and ( + "override" in config[file.name].keys() + or "Folder" in config[file.name].keys() + ) + ): + return get_module_short_desc(config, file) + meta_file.parent.mkdir(parents=True, exist_ok=True) + meta_file.unlink(missing_ok=True) + out_dir = ( + str(meta_file)[:-4] if self._utils.extract_full_package else "" + ) + can_continue = True + if self.run_divine( + f'{"extract-package" if self._utils.extract_full_package else "extract-single-file -f meta.lsx"} -d "{meta_file if not self._utils.extract_full_package else out_dir}"', + file, + ).returncode: + can_continue = False + if can_continue and self._utils.extract_full_package: + qDebug(f"archive {file} extracted to {out_dir}") + if self.run_divine( + f'convert-resources -d "{out_dir}" -i lsf -o lsx -x "*.lsf"', + out_dir, + ).returncode: + qDebug( + f"failed to convert lsf files in {out_dir} to readable lsx" + ) + extracted_meta_files = list(Path(out_dir).rglob("meta.lsx")) + if len(extracted_meta_files) == 0: + qInfo( + f"No meta.lsx files found in {file.name}, {file.name} determined to be an override mod" + ) + can_continue = False + else: + shutil.copyfile( + extracted_meta_files[0], + meta_file, + ) + elif can_continue and not meta_file.exists(): + qInfo( + f"No meta.lsx files found in {file.name}, {file.name} determined to be an override mod" + ) + can_continue = False + return self.metadata_to_ini( + config, file, mod, meta_ini, can_continue, lambda: meta_file + ) + finally: + if self._utils.remove_extracted_metadata: + meta_file.unlink(missing_ok=True) + if self._utils.extract_full_package: + Path(str(meta_file)[:-4]).unlink(missing_ok=True) + elif file.is_dir(): + if self._folder_pattern.search(file.name): + return "" + for folder in bg3_utils.loose_file_folders: + if next(file.glob(f"{folder}/*"), False): + break + else: + return "" + qInfo(f"packable dir: {file}") + if (file.parent / f"{file.name}.pak").exists() or ( + file.parent / "Mods" / f"{file.name}.pak" + ).exists(): + qInfo( + f"pak with same name as packable dir exists in mod directory. not packing dir {file}" + ) + return "" + parent_mod_name = file.parent.name.replace(" ", "_") + pak_path = ( + self._utils.overwrite_path + / f"Mods/{parent_mod_name}_{file.name}.pak" + ) + build_pak = True + if pak_path.exists(): + try: + pak_creation_time = os.path.getmtime(pak_path) + for root, _, files in file.walk(): + for f in files: + file_path = root.joinpath(f) + try: + if os.path.getmtime(file_path) > pak_creation_time: + break + except OSError as e: + qDebug(f"Error accessing file {file_path}: {e}") + break + else: + build_pak = False + except OSError as e: + qDebug(f"Error accessing file {pak_path}: {e}") + build_pak = False + if build_pak: + pak_path.unlink(missing_ok=True) + if self.run_divine( + f'create-package -d "{pak_path}"', file + ).returncode: + return "" + meta_files = list(file.glob("Mods/*/meta.lsx")) + return self.metadata_to_ini( + config, + file, + mod, + meta_ini, + len(meta_files) > 0, + lambda: meta_files[0], + ) + else: + return "" + except Exception: + qWarning(traceback.format_exc()) + return "" + + def run_divine( + self, action: str, source: Path | str + ) -> subprocess.CompletedProcess[str]: + command = f'{self._divine_command} -a {action} -s "{source}"' + result = subprocess.run( + command, + creationflags=subprocess.CREATE_NO_WINDOW, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode: + qWarning( + f"{command.replace(str(Path.home()), '~', 1).replace(str(Path.home()), '$HOME')}" + f" returned stdout: {result.stdout}, stderr: {result.stderr}, code {result.returncode}" + ) + return result + + def get_attr_value(self, root: Element, attr_id: str) -> str: + default_val = self._types.get(attr_id) or "" + attr = root.find(f".//attribute[@id='{attr_id}']") + return default_val if attr is None else attr.get("value", default_val) + + def metadata_to_ini( + self, + config: configparser.ConfigParser, + file: Path, + mod: mobase.IModInterface, + meta_ini: Path, + condition: bool, + to_parse: Callable[[], Path], + ): + config[file.name] = {} + if condition: + root = ( + ElementTree.parse(to_parse()) + .getroot() + .find(".//node[@id='ModuleInfo']") + ) + if root is None: + qInfo(f"No ModuleInfo node found in meta.lsx for {mod.name()} ") + else: + section = config[file.name] + folder_name = self.get_attr_value(root, "Folder") + if file.is_dir(): + self._mod_cache[file] = ( + len(list(file.glob(f"*/{folder_name}/**"))) > 1 + or len( + list(file.glob("Public/Engine/Timeline/MaterialGroups/*")) + ) + > 0 + ) + elif file not in self._mod_cache: + # a mod which has a meta.lsx and is not an override mod meets at least one of three conditions: + # 1. it has files in Public/Engine/Timeline/MaterialGroups, or + # 2. it has files in Mods// other than the meta.lsx file, or + # 3. it has files in Public/ + result = self.run_divine( + f'list-package --use-regex -x "(/{re.escape(folder_name)}/(?!meta\\.lsx))|(Public/Engine/Timeline/MaterialGroups)"', + file, + ) + self._mod_cache[file] = ( + result.returncode == 0 and result.stdout.strip() != "" + ) + if self._mod_cache[file]: + for key in self._types: + section[key] = self.get_attr_value(root, key) + else: + qInfo(f"pak {file.name} determined to be an override mod") + section["override"] = "True" + section["Folder"] = folder_name + else: + config[file.name]["override"] = "True" + with open(meta_ini, "w+", encoding="utf-8") as f: + config.write(f) + return get_module_short_desc(config, file) + + +def get_module_short_desc(config: configparser.ConfigParser, file: Path) -> str: + if not config.has_section(file.name): + return "" + section: configparser.SectionProxy = config[file.name] + return ( + "" + if "override" in section.keys() or "Name" not in section.keys() + else bg3_utils.get_node_string( + folder=section["Folder"], + md5=section["MD5"], + name=section["Name"], + publish_handle=section["PublishHandle"], + uuid=section["UUID"], + version64=section["Version64"], + ) + ) diff --git a/libs/basic_games/games/baldursgate3/plugins/__init__.py b/libs/basic_games/games/baldursgate3/plugins/__init__.py new file mode 100644 index 0000000..1b69faf --- /dev/null +++ b/libs/basic_games/games/baldursgate3/plugins/__init__.py @@ -0,0 +1,13 @@ +import mobase + +from .check_for_lslib_updates_plugin import BG3ToolCheckForLsLibUpdates +from .convert_jsons_to_yaml_plugin import BG3ToolConvertJsonsToYaml +from .reparse_pak_metadata_plugin import BG3ToolReparsePakMetadata + + +def createPlugins() -> list[mobase.IPluginTool]: + return [ + BG3ToolCheckForLsLibUpdates(), + BG3ToolReparsePakMetadata(), + BG3ToolConvertJsonsToYaml(), + ] diff --git a/libs/basic_games/games/baldursgate3/plugins/bg3_tool_plugin.py b/libs/basic_games/games/baldursgate3/plugins/bg3_tool_plugin.py new file mode 100644 index 0000000..fc258a6 --- /dev/null +++ b/libs/basic_games/games/baldursgate3/plugins/bg3_tool_plugin.py @@ -0,0 +1,49 @@ +from PyQt6.QtCore import QCoreApplication +from PyQt6.QtGui import QIcon, QPixmap + +import mobase + + +class BG3ToolPlugin(mobase.IPluginTool, mobase.IPlugin): + desc = sub_name = "" + icon_bytes: bytes + + def __init__(self): + mobase.IPluginTool.__init__(self) + mobase.IPlugin.__init__(self) + self._pluginName = self._displayName = "BG3 Tools" + self._pluginVersion = mobase.VersionInfo(1, 0, 0) + pixmap = QPixmap() + pixmap.loadFromData(self.icon_bytes, "SVG") + self.qicon = QIcon(pixmap) + + def init(self, organizer: mobase.IOrganizer) -> bool: + self._organizer = organizer + return True + + def version(self): + return self._pluginVersion + + def author(self): + return "daescha" + + def name(self): + return f"{self._pluginName}: {self.sub_name}" + + def displayName(self): + return f"{self._displayName}/{self.sub_name}" + + def tooltip(self): + return self.description() + + def enabledByDefault(self): + return self._organizer.managedGame().name() == "Baldur's Gate 3 Plugin" + + def settings(self) -> list[mobase.PluginSetting]: + return [] + + def icon(self) -> QIcon: + return self.qicon + + def description(self) -> str: + return QCoreApplication.translate(self._pluginName, self.desc) diff --git a/libs/basic_games/games/baldursgate3/plugins/check_for_lslib_updates_plugin.py b/libs/basic_games/games/baldursgate3/plugins/check_for_lslib_updates_plugin.py new file mode 100644 index 0000000..b534e8a --- /dev/null +++ b/libs/basic_games/games/baldursgate3/plugins/check_for_lslib_updates_plugin.py @@ -0,0 +1,15 @@ +from .bg3_tool_plugin import BG3ToolPlugin +from .icons import download + + +class BG3ToolCheckForLsLibUpdates(BG3ToolPlugin): + icon_bytes = download + sub_name = "Check For LsLib Updates" + desc = "Check to see if there has been a new release of LSLib and create download dialog if so." + + def display(self): + from ...game_baldursgate3 import BG3Game + + game_plugin = self._organizer.managedGame() + if isinstance(game_plugin, BG3Game): + game_plugin.utils.lslib_retriever.download_lslib_if_missing(True) diff --git a/libs/basic_games/games/baldursgate3/plugins/convert_jsons_to_yaml_plugin.py b/libs/basic_games/games/baldursgate3/plugins/convert_jsons_to_yaml_plugin.py new file mode 100644 index 0000000..fde3431 --- /dev/null +++ b/libs/basic_games/games/baldursgate3/plugins/convert_jsons_to_yaml_plugin.py @@ -0,0 +1,58 @@ +import json +import os +from pathlib import Path + +from PyQt6.QtCore import qInfo, qWarning +from PyQt6.QtWidgets import QApplication + +from .bg3_tool_plugin import BG3ToolPlugin +from .icons import exchange + + +class BG3ToolConvertJsonsToYaml(BG3ToolPlugin): + icon_bytes = exchange + sub_name = "Convert JSONS to YAML" + desc = "Convert all jsons in active mods to yaml immediately." + + def display(self): + from ...game_baldursgate3 import BG3Game + + game_plugin = self._organizer.managedGame() + if not isinstance(game_plugin, BG3Game): + return + utils = game_plugin.utils + qInfo("converting all json files to yaml") + active_mods = utils.active_mods() + progress = utils.create_progress_window( + "Converting all json files to yaml", len(active_mods) + 1 + ) + for mod in active_mods: + _convert_jsons_in_dir_to_yaml(Path(mod.absolutePath())) + progress.setValue(progress.value() + 1) + QApplication.processEvents() + if progress.wasCanceled(): + qWarning("conversion canceled by user") + return + _convert_jsons_in_dir_to_yaml(utils.overwrite_path) + progress.setValue(len(active_mods) + 1) + QApplication.processEvents() + progress.close() + + +def _convert_jsons_in_dir_to_yaml(path: Path): + for file in list(path.rglob("*.json")): + converted_path = file.parent / file.name.replace(".json", ".yaml") + try: + if not converted_path.exists() or os.path.getmtime(file) > os.path.getmtime( + converted_path + ): + import yaml + + with open(file, "r") as json_file: + with open(converted_path, "w") as yaml_file: + yaml.dump( + json.load(json_file), yaml_file, indent=2, sort_keys=False + ) + qInfo(f"Converted {file} to YAML") + except OSError as e: + qWarning(f"Error accessing file {converted_path}: {e}") diff --git a/libs/basic_games/games/baldursgate3/plugins/icons.py b/libs/basic_games/games/baldursgate3/plugins/icons.py new file mode 100644 index 0000000..adc1023 --- /dev/null +++ b/libs/basic_games/games/baldursgate3/plugins/icons.py @@ -0,0 +1,42 @@ +refresh = b""" + + + + + + + +""" + +exchange = b""" + + + + + + + + + + +""" + +download = b""" + + + + + + + + + + +""" diff --git a/libs/basic_games/games/baldursgate3/plugins/reparse_pak_metadata_plugin.py b/libs/basic_games/games/baldursgate3/plugins/reparse_pak_metadata_plugin.py new file mode 100644 index 0000000..f32a8a0 --- /dev/null +++ b/libs/basic_games/games/baldursgate3/plugins/reparse_pak_metadata_plugin.py @@ -0,0 +1,17 @@ +from .bg3_tool_plugin import BG3ToolPlugin +from .icons import refresh + + +class BG3ToolReparsePakMetadata(BG3ToolPlugin): + icon_bytes = refresh + sub_name = "Reparse Pak Metadata" + desc = "Force reparsing mod metadata immediately." + + def display(self): + from ...game_baldursgate3 import BG3Game + + game_plugin = self._organizer.managedGame() + if isinstance(game_plugin, BG3Game): + game_plugin.utils.construct_modsettings_xml( + exec_path="bin/bg3", force_reparse_metadata=True + ) diff --git a/libs/basic_games/games/game_arkhamcity.py b/libs/basic_games/games/game_arkhamcity.py new file mode 100644 index 0000000..0b8269f --- /dev/null +++ b/libs/basic_games/games/game_arkhamcity.py @@ -0,0 +1,87 @@ +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_features import BasicLocalSavegames +from ..basic_game import BasicGame +from ..steam_utils import find_steam_path + + +# Lifted from https://github.com/ModOrganizer2/modorganizer-basic_games/blob/71dbb8c557d43cba9d290674a332e7ecd1650261/games/game_darkestdungeon.py +class ArkhamCityModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + self.validDirNames = [ + "config", + "cookedpcconsole", + "localization", + "movies", + "moviesstereo", + "splash", + ] + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + for entry in filetree: + if not entry.isDir(): + continue + if entry.name().casefold() in self.validDirNames: + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.INVALID + + +class ArkhamCityGame(BasicGame): + Name = "Batman: Arkham City Plugin" + Author = "Paynamia" + Version = "0.5.3" + + GameName = "Batman: Arkham City" + GameShortName = "batmanarkhamcity" + GameNexusId = 372 + GameSteamId = 200260 + GameGogId = 1260066469 + GameEpicId = "Egret" + GameBinary = "Binaries/Win32/BatmanAC.exe" + GameLauncher = "Binaries/Win32/BmLauncher.exe" + GameDataPath = "BmGame" + GameDocumentsDirectory = ( + "%DOCUMENTS%/WB Games/Batman Arkham City GOTY/BmGame/Config" + ) + GameIniFiles = ["UserEngine.ini", "UserGame.ini", "UserInput.ini"] + GameSaveExtension = "sgd" + + # This will only detect saves from the earliest-created Steam profile on the user's PC. + def savesDirectory(self) -> QDir: + docSaves = QDir(self.documentsDirectory().cleanPath("../../SaveData")) + if self.is_steam(): + if (steamDir := find_steam_path()) is None: + return docSaves + for child in steamDir.joinpath("userdata").iterdir(): + if not child.is_dir() or child.name == "0": + continue + steamSaves = child.joinpath("200260", "remote") + if steamSaves.is_dir(): + return QDir(str(steamSaves)) + else: + return docSaves + else: + return docSaves + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(ArkhamCityModDataChecker()) + self._register_feature(BasicLocalSavegames(self)) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "Batman: Arkham City", + QFileInfo(self.gameDirectory(), "Binaries/Win32/BatmanAC.exe"), + ), + mobase.ExecutableInfo( + "Arkham City Launcher", + QFileInfo(self.gameDirectory(), "Binaries/Win32/BmLauncher.exe"), + ), + ] diff --git a/libs/basic_games/games/game_assettocorsa.py b/libs/basic_games/games/game_assettocorsa.py new file mode 100644 index 0000000..da70669 --- /dev/null +++ b/libs/basic_games/games/game_assettocorsa.py @@ -0,0 +1,18 @@ +from ..basic_game import BasicGame + + +class AssettoCorsaGame(BasicGame): + Name = "Assetto Corsa Support Plugin" + Author = "Deorder" + Version = "0.0.1" + + GameName = "Assetto Corsa" + GameShortName = "ac" + GameBinary = r"AssettoCorsa.exe" + GameDataPath = r"" + GameSteamId = 244210 + GameDocumentsDirectory = "%DOCUMENTS%/Assetto Corsa" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Assetto-Corsa" + ) diff --git a/libs/basic_games/games/game_baldursgate3.py b/libs/basic_games/games/game_baldursgate3.py new file mode 100644 index 0000000..4898696 --- /dev/null +++ b/libs/basic_games/games/game_baldursgate3.py @@ -0,0 +1,239 @@ +import datetime +import difflib +import os +import shutil +from functools import cached_property +from pathlib import Path +from typing import Any + +from PyQt6.QtCore import QLoggingCategory, qDebug, qInfo + +import mobase + +from ..basic_features import BasicGameSaveGameInfo, BasicLocalSavegames +from ..basic_game import BasicGame +from .baldursgate3 import bg3_file_mapper + + +class BG3Game(BasicGame, bg3_file_mapper.BG3FileMapper): + Name = "Baldur's Gate 3 Plugin" + Author = "daescha" + Version = "0.1.0" + GameName = "Baldur's Gate 3" + GameShortName = "baldursgate3" + GameNexusName = "baldursgate3" + GameValidShortNames = ["bg3"] + GameLauncher = "Launcher/LariLauncher.exe" + GameBinary = "bin/bg3.exe" + GameDataPath = "" + GameDocumentsDirectory = ( + "%USERPROFILE%/AppData/Local/Larian Studios/Baldur's Gate 3" + ) + GameSavesDirectory = "%GAME_DOCUMENTS%/PlayerProfiles/Public/Savegames/Story" + GameSaveExtension = "lsv" + GameNexusId = 3474 + GameSteamId = 1086940 + GameGogId = 1456460669 + + def __init__(self): + BasicGame.__init__(self) + from .baldursgate3 import bg3_utils + + self.utils = bg3_utils.BG3Utils(self.name()) + bg3_file_mapper.BG3FileMapper.__init__( + self, self.utils, self.documentsDirectory + ) + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.utils.init(organizer) + from .baldursgate3 import bg3_data_checker, bg3_data_content + + self._register_feature(bg3_data_checker.BG3ModDataChecker()) + self._register_feature(bg3_data_content.BG3DataContent()) + self._register_feature(BasicGameSaveGameInfo(lambda s: s.with_suffix(".webp"))) + self._register_feature(BasicLocalSavegames(self)) + organizer.onAboutToRun(self.utils.construct_modsettings_xml) + organizer.onFinishedRun(self._on_finished_run) + organizer.onUserInterfaceInitialized(self.utils.on_user_interface_initialized) + organizer.modList().onModInstalled(self.utils.on_mod_installed) + organizer.onPluginSettingChanged(self.utils.on_settings_changed) + return True + + def settings(self): + base_settings = super().settings() + custom_settings = [ + mobase.PluginSetting( + "force_load_dlls", + "Force load all dlls detected in active mods. Removes the need for 'Native Mod Loader' and similar mods.", + True, + ), + mobase.PluginSetting( + "log_diff", + "Log a diff of the modsettings.xml file before and after the game runs to check for differences.", + False, + ), + mobase.PluginSetting( + "delete_levelcache_folders_older_than_x_days", + "Maximum number of days a file in overwrite/LevelCache is allowed to exist before being deleted " + "after the executable finishes. Set to negative to disable.", + 3, + ), + mobase.PluginSetting( + "autobuild_paks", + "Autobuild folders likely to be PAK folders with every run of an executable.", + True, + ), + mobase.PluginSetting( + "remove_extracted_metadata", + "Remove extracted meta.lsx files when they are no longer needed.", + True, + ), + mobase.PluginSetting( + "extract_full_package", + "Extract the full pak when parsing metadata, instead of just meta.lsx.", + False, + ), + mobase.PluginSetting( + "convert_yamls_to_json", + "Convert YAMLs to JSONs when executable runs. Allows one to configure ScriptExtender and related mods with YAML files.", + False, + ), + ] + for setting in custom_settings: + setting.description = self.utils.tr(setting.description) + base_settings.append(setting) + return base_settings + + def executables(self) -> list[mobase.ExecutableInfo]: + return [ + mobase.ExecutableInfo( + f"{self.gameName()}: DX11", + self.gameDirectory().absoluteFilePath("bin/bg3_dx11.exe"), + ), + mobase.ExecutableInfo( + f"{self.gameName()}: Vulkan", + self.gameDirectory().absoluteFilePath(self.binaryName()), + ), + mobase.ExecutableInfo( + "Larian Launcher", + self.gameDirectory().absoluteFilePath(self.getLauncherName()), + ), + ] + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + if self.utils.force_load_dlls: + qInfo("detecting dlls in enabled mods") + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree().find("bin") + ) + if type(tree) is not mobase.IFileTree: + return efls + + def find_dlls( + _: Any, entry: mobase.FileTreeEntry + ) -> mobase.IFileTree.WalkReturn: + relpath = entry.pathFrom(tree) + if ( + relpath + and entry.hasSuffix("dll") + and relpath not in self._base_dlls + ): + libs.add(relpath) + return mobase.IFileTree.WalkReturn.CONTINUE + + tree.walk(find_dlls) + exes = self.executables() + qDebug(f"dlls to force load: {libs}") + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] + return efls + + def loadOrderMechanism(self) -> mobase.LoadOrderMechanism: + return mobase.LoadOrderMechanism.PLUGINS_TXT + + @cached_property + def _base_dlls(self) -> set[str]: + base_bin = Path(self.gameDirectory().absoluteFilePath("bin")) + return {str(f.relative_to(base_bin)) for f in base_bin.glob("*.dll")} + + def _on_finished_run(self, exec_path: str, exit_code: int): + if "bin/bg3" not in exec_path: + return + cat = QLoggingCategory.defaultCategory() + self.utils.log_dir.mkdir(parents=True, exist_ok=True) + if ( + cat is not None + and cat.isDebugEnabled() + and self.utils.log_diff + and self.utils.modsettings_backup.exists() + and self.utils.modsettings_path.exists() + ): + for x in difflib.unified_diff( + self.utils.modsettings_backup.open().readlines(), + self.utils.modsettings_path.open().readlines(), + fromfile=str(self.utils.modsettings_backup), + tofile=str(self.utils.modsettings_path), + lineterm="", + ): + qDebug(x) + moved: dict[str, str] = {} + for path in self.utils.overwrite_path.rglob("*.log"): + try: + moved[str(path.relative_to(Path.home()))] = str( + (self.utils.log_dir / path.name).relative_to(Path.home()) + ) + path.replace(self.utils.log_dir / path.name) + except PermissionError as e: + qDebug(str(e)) + for path in self.utils.overwrite_path.rglob("*log.txt"): + dest = self.utils.log_dir / path.name + if path.name == "log.txt": + dest = self.utils.log_dir / f"{path.parent.name}-{path.name}" + try: + moved[str(path.relative_to(Path.home()))] = str( + dest.relative_to(Path.home()) + ) + path.replace(dest) + except PermissionError as e: + qDebug(str(e)) + if cat is not None and cat.isDebugEnabled() and len(moved) > 0: + qDebug(f"moved log files to logs dir: {moved}") + days = self.utils.get_setting("delete_levelcache_folders_older_than_x_days") + if type(days) is int and days >= 0: + cutoff_time = datetime.datetime.now() - datetime.timedelta(days=days) + qDebug(f"cleaning folders in overwrite/LevelCache older than {cutoff_time}") + removed: set[Path] = set() + for path in self.utils.overwrite_path.glob("LevelCache/*"): + if ( + datetime.datetime.fromtimestamp(os.path.getmtime(path)) + < cutoff_time + ): + shutil.rmtree(path, ignore_errors=True) + removed.add(path) + if cat is not None and cat.isDebugEnabled() and len(removed) > 0: + qDebug( + f"cleaned the following folders due to them being older than {cutoff_time}: {removed}" + ) + for fdir in {self.utils.overwrite_path, self.doc_path}: + removed: set[Path] = set() + for folder in sorted(list(fdir.walk(top_down=False)))[:-1]: + try: + folder[0].rmdir() + removed.add(folder[0].relative_to(Path.home())) + except OSError: + pass + if cat is not None and cat.isDebugEnabled() and len(removed) > 0: + qDebug( + f"cleaned empty dirs from {fdir.relative_to(Path.home())} {removed}" + ) diff --git a/libs/basic_games/games/game_blackandwhite2.py b/libs/basic_games/games/game_blackandwhite2.py new file mode 100644 index 0000000..5d9f60b --- /dev/null +++ b/libs/basic_games/games/game_blackandwhite2.py @@ -0,0 +1,341 @@ +import datetime +import os +import struct +import time +from collections.abc import Mapping +from pathlib import Path +from typing import BinaryIO + +from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo + +import mobase + +from ..basic_features import BasicLocalSavegames +from ..basic_features.basic_save_game_info import ( + BasicGameSaveGame, + BasicGameSaveGameInfo, + format_date, +) +from ..basic_game import BasicGame + + +class BlackAndWhite2ModDataChecker(mobase.ModDataChecker): + _validFolderTree = { + "": ["audio", "data", "plugins", "scripts"], + "audio": ["dialogue", "music", "sfx"], + "music": [ + "buildingmusic", + "chant", + "cutscene", + "dynamic music", + "epicspell", + "townalignment", + ], + "sfx": ["atmos", "creature", "game", "script", "spells", "video", "grass"], + "data": [ + "art", + "balance", + "ctr", + "effects", + "encryptedshaders", + "font", + "handdemo", + "interface", + "landscape", + "light particle effects", + "lipsync", + "physics", + "sfx", + "shaders", + "symbols", + "text", + "textures", + "tutorial avi", + "visualeffects", + "weathersystem", + "zones", + ], + "art": [ + "binary_anim_libs", + "binary_animations", + "features", + "models", + "skins", + "textures", + "water", + ], + "ctr": [ + "badvisor_evil", + "badvisor_good", + "bape", + "bgorilla", + "bhand", + "blion", + "btiger", + "bwolf", + "damage", + "siren", + ], + "font": ["asian"], + "asian": ["korean", "traditional chinese"], + "landscape": [ + "aztec", + "bw2", + "egyptian", + "generic", + "greek", + "japanese", + "norse", + "skysettings", + ], + "tutorial avi": ["placeholder", "stills"], + "visualeffects": ["textures"], + "scripts": ["bw2"], + } + _validFileLocation = { + "": ["exe", "dll", "ico", "png", "jpeg", "jpg"] + } + _mapFile = ["chl", "bmp", "bwe", "ter", "pat", "xml", "wal", "txt"] + _fileIgnore = ["readme", "read me", "meta.ini", "thumbs.db", "backup", ".png"] + + def fix(self, filetree: mobase.IFileTree): + toMove: list[tuple[mobase.FileTreeEntry, str]] = [] + for entry in filetree: + if any([sub in entry.name().casefold() for sub in self._fileIgnore]): + continue + elif entry.suffix() == "chl": + toMove.append((entry, "/Scripts/BW2/")) + elif entry.suffix() == "bmp": + toMove.append((entry, "/Data/")) + elif entry.suffix() == "txt": + toMove.append((entry, "/Scripts/")) + else: + toMove.append((entry, "/Data/landscape/BW2/")) + + for entry, path in toMove: + filetree.move(entry, path, policy=mobase.IFileTree.MERGE) + + return filetree + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + # qInfo("Data validation start") + root = filetree + unpackagedMap = False + + for entry in filetree: + entryName = entry.name().casefold() + canIgnore = any([sub in entryName for sub in self._fileIgnore]) + if not canIgnore: + parent = entry.parent() + if parent is not None: + if parent != root: + parentName = parent.name().casefold() + else: + # qInfo(str(entryName)) + parentName = "" + + if not entry.isDir(): + if parentName in self._validFileLocation.keys(): + if ( + entry.suffix() + not in self._validFileLocation[parentName] + ): + if ( + entry.suffix() in self._mapFile + or entryName == "map.txt" + ): + unpackagedMap = True + else: + return mobase.ModDataChecker.INVALID + else: + unpackagedMap = False + if parentName in self._validFolderTree.keys(): + if entryName not in self._validFolderTree[parentName]: + return mobase.ModDataChecker.INVALID + + # qInfo(str(unpackagedMap)) + if unpackagedMap: + return mobase.ModDataChecker.FIXABLE + else: + return mobase.ModDataChecker.VALID + + +class BlackAndWhite2SaveGame(BasicGameSaveGame): + _saveInfLayout = { + "start": [0x00000000, 0x00000004], + "name": [0x00000004, 0x0000002C], + "empty": [0x0000002C, 0x00000104], + "land": [0x00000104, 0x00000108], + "date": [0x00000108, 0x00000110], + "empty1": [0x00000110, 0x00000114], + "elapsed": [0x00000114, 0x00000118], + "empty2": [0x00000108, 0x0000011C], + } + + def __init__(self, filepath: Path): + super().__init__(filepath) + self._filepath = Path(filepath) + self.name: str = "" + self.land: int = -1 + self.elapsed: int = 0 + self.lastsave: int = 0 + with open(self._filepath.joinpath("SaveGame.inf"), "rb") as info: + # Name embedded in "SaveGame.inf" with UTF-16 encoding + self.name = self.readInf(info, "name").decode("utf-16") + # Land number embedded in "SaveGame.inf" as an int written in binary + self.land = int.from_bytes(self.readInf(info, "land"), "little") + # Getting elapsed time in second + self.elapsed = int.from_bytes(self.readInf(info, "elapsed"), "little") + # Getting date in 100th of nanosecond need to convert NT time + # to UNIX time and offset localtime + self.lastsave = int( + ( + struct.unpack("q", self.readInf(info, "date"))[0] / 10000 + - 11644473600000 + ) + - (time.localtime().tm_gmtoff * 1000) + ) + info.close() + + def readInf(self, inf: BinaryIO, key: str): + inf.seek(self._saveInfLayout[key][0]) + return inf.read(self._saveInfLayout[key][1] - self._saveInfLayout[key][0]) + + def allFiles(self) -> list[str]: + files = [str(file) for file in self._filepath.glob("./*")] + files.append(str(self._filepath)) + return files + + def getCreationTime(self) -> QDateTime: + return QDateTime.fromMSecsSinceEpoch(self.lastsave) + + def getElapsed(self) -> str: + return str(datetime.timedelta(seconds=self.elapsed)) + + def getName(self) -> str: + return self.name + + def getLand(self) -> str: + return str(self.land) + + def getSaveGroupIdentifier(self): + return self._filepath.parent.parent.name + + +def getMetadata(savepath: Path, save: mobase.ISaveGame) -> Mapping[str, str]: + assert isinstance(save, BlackAndWhite2SaveGame) + return { + "Name": save.getName(), + "Profile": save.getSaveGroupIdentifier(), + "Land": save.getLand(), + "Saved at": format_date(save.getCreationTime()), + "Elapsed time": save.getElapsed(), + } + + +PSTART_MENU = ( + str(os.getenv("ProgramData")) + "\\Microsoft\\Windows\\Start Menu\\Programs" +) + + +class BlackAndWhite2Game(BasicGame): + Name = "Black & White 2 Support Plugin" + Author = "Ilyu" + Version = "1.0.1" + + GameName = "Black & White 2" + GameShortName = "BW2" + GameNexusName = "blackandwhite2" + GameDataPath = "%GAME_PATH%" + GameBinary = "white.exe" + GameDocumentsDirectory = "%DOCUMENTS%/Black & White 2" + GameSavesDirectory = "%GAME_DOCUMENTS%/Profiles" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Black-&-White-2" + ) + + _program_link = PSTART_MENU + "\\Black & White 2\\Black & White® 2.lnk" + + def init(self, organizer: mobase.IOrganizer) -> bool: + BasicGame.init(self, organizer) + + self._register_feature(BlackAndWhite2ModDataChecker()) + self._register_feature(BasicLocalSavegames(self)) + self._register_feature( + BasicGameSaveGameInfo(get_metadata=getMetadata, max_width=400) + ) + return True + + def detectGame(self): + super().detectGame() + + program_path = Path(self._program_link) + if program_path.exists(): + installation_path = Path(QFileInfo(self._program_link).symLinkTarget()) + if installation_path.exists(): + self.setGamePath(installation_path.parent) + + return + + def executables(self) -> list[mobase.ExecutableInfo]: + execs = super().executables() + + """ + A bat file to load modded executables from VFS. + """ + workaroundPath = self._gamePath + "/" + self.GameBinary[:-4] + ".bat" + + try: + workaround = open(workaroundPath, "rt") + except FileNotFoundError: + with open(workaroundPath, "wt") as workaround: + workaround.write('start "" "' + self.GameBinary + '"') + workaround.close() + + execs.append( + mobase.ExecutableInfo( + self.GameShortName + " Modded Exec", QFileInfo(workaroundPath) + ) + ) + + return execs + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + profiles: list[Path] = [] + for path in Path(folder.absolutePath()).glob("*/Saved Games/*"): + if ( + path.name == "Autosave" + or path.name == "Pictures" + or "_invalid" in path.name + ): + continue + if path.is_dir(): + saveFolder = QDir(str(path)) + if not saveFolder.exists("SaveGame.inf"): + savePath = saveFolder.absolutePath() + QFile.rename(savePath, savePath + "_invalid") + continue + else: + continue + + profiles.append(path) + + return [BlackAndWhite2SaveGame(path) for path in profiles] + + +class BOTGGame(BlackAndWhite2Game): + Name = "Black & White 2 Battle of the Gods Support Plugin" + + GameName = "Black & White 2 Battle of the Gods" + GameShortName = "BOTG" + GameBinary = "BattleOfTheGods.exe" + GameDocumentsDirectory = "%DOCUMENTS%/Black & White 2 - Battle of the Gods" + GameSavesDirectory = "%GAME_DOCUMENTS%/Profiles" + + _program_link = ( + PSTART_MENU + "\\Black & White 2 Battle of the Gods" + "\\Black & White® 2 Battle of the Gods.lnk" + ) diff --git a/libs/basic_games/games/game_bladeandsorcery.py b/libs/basic_games/games/game_bladeandsorcery.py new file mode 100644 index 0000000..bc689eb --- /dev/null +++ b/libs/basic_games/games/game_bladeandsorcery.py @@ -0,0 +1,98 @@ +import json +from collections.abc import Mapping +from pathlib import Path + +from PyQt6.QtCore import QDateTime, QDir + +import mobase + +from ..basic_features.basic_save_game_info import ( + BasicGameSaveGame, + BasicGameSaveGameInfo, + format_date, +) +from ..basic_game import BasicGame + + +class BaSSaveGame(BasicGameSaveGame): + def __init__(self, filepath: Path): + super().__init__(filepath) + with open(self._filepath, "rb") as save: + save_data = json.load(save) + self._gameMode = save_data["mode"]["saveData"]["gameModeId"] + self._gender = ( + "Male" + if save_data["customization"]["creatureId"] == "PlayerDefaultMale" + else "Female" + ) + self._ethnicity = save_data["customization"]["ethnicGroupId"] + h, m, s = save_data["playTime"].split(":") + self._elapsed = (float(h), int(m), float(s)) + f_stat = self._filepath.stat() + self._created = f_stat.st_birthtime + self._modified = f_stat.st_mtime + + def getName(self) -> str: + return f"{self.getPlayerSlug()} - {self._gameMode}" + + def getCreationTime(self) -> QDateTime: + return QDateTime.fromSecsSinceEpoch(int(self._created)) + + def getModifiedTime(self) -> QDateTime: + return QDateTime.fromSecsSinceEpoch(int(self._modified)) + + def getPlayerSlug(self) -> str: + return f"{self._gender} {self._ethnicity}" + + def getElapsed(self) -> str: + return ( + f"{self._elapsed[0]} hours, " + f"{self._elapsed[1]} minutes, " + f"{int(self._elapsed[2])} seconds" + ) + + def getGameMode(self) -> str: + return self._gameMode + + +def bas_parse_metadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str]: + assert isinstance(save, BaSSaveGame) + return { + "Character": save.getPlayerSlug(), + "Game Mode": save.getGameMode(), + "Created At": format_date(save.getCreationTime()), + "Last Saved": format_date(save.getModifiedTime()), + "Session Duration": save.getElapsed(), + } + + +class BaSGame(BasicGame): + Name = "Blade & Sorcery Plugin" + Author = "R3z Shark & Silarn & Jonny_Bro" + Version = "0.5.1" + + GameName = "Blade & Sorcery" + GameShortName = "bladeandsorcery" + GameBinary = "BladeAndSorcery.exe" + GameDataPath = r"BladeAndSorcery_Data\\StreamingAssets\\Mods" + GameDocumentsDirectory = "%DOCUMENTS%/My Games/BladeAndSorcery" + GameSavesDirectory = "%GAME_DOCUMENTS%/Saves/Default" + GameSaveExtension = "chr" + GameSteamId = 629730 + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Blade-&-Sorcery" + ) + + def init(self, organizer: mobase.IOrganizer) -> bool: + BasicGame.init(self, organizer) + self._register_feature( + BasicGameSaveGameInfo(get_metadata=bas_parse_metadata, max_width=400) + ) + return True + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + BaSSaveGame(path) for path in Path(folder.absolutePath()).glob(f"*.{ext}") + ] diff --git a/libs/basic_games/games/game_borderlands1.py b/libs/basic_games/games/game_borderlands1.py new file mode 100644 index 0000000..4b90b51 --- /dev/null +++ b/libs/basic_games/games/game_borderlands1.py @@ -0,0 +1,153 @@ +import re + +import mobase +from mobase import FileTreeEntry, IFileTree, ModDataChecker + +from ..basic_features import BasicModDataChecker +from ..basic_game import BasicGame + +_extention_pattern = re.compile("\\.(upk|umap|u|int|dll|exe)$", re.I) +_mapslot_pattern = re.compile("^Mapslot\\d\\d?\\.umap$", re.I) + +_mod_dirs = { + "Binaries".casefold(): "/", + "WillowGame".casefold(): "/", + "CookedPC".casefold(): "WillowGame/", + "Localization".casefold(): "WillowGame/", + "Maps".casefold(): "WillowGame/CookedPC/", +} + +_slots_path = "WillowGame/CookedPC/Maps/MapSlots" + + +def _check_filetree( + filetree: IFileTree | FileTreeEntry, recurse: bool +) -> ModDataChecker.CheckReturn: + if filetree.isFile(): + if _extention_pattern.search(filetree.name()): + if _mapslot_pattern.search(filetree.name()): + return ModDataChecker.FIXABLE + else: + return ModDataChecker.INVALID + elif recurse and isinstance(filetree, IFileTree): + status = ModDataChecker.VALID + for child in filetree: + child_status = _check_filetree(child, True) + if child_status is ModDataChecker.INVALID: + return ModDataChecker.INVALID + elif child_status is ModDataChecker.FIXABLE: + status = ModDataChecker.FIXABLE + return status + + return ModDataChecker.VALID + + +def _get_nest(filetree: IFileTree) -> IFileTree | None: + children = tuple(filetree) + if ( + len(children) == 1 + and children[0].isDir() + and isinstance(children[0], IFileTree) + and _mod_dirs.get(children[0].name().casefold()) is None + ): + return children[0] + return None + + +def _get_slotstree(roottree: IFileTree) -> IFileTree: + slotstree: IFileTree | None = roottree.find(_slots_path) # type: ignore + if slotstree is None: + slotstree = roottree.addDirectory(_slots_path) + return slotstree + + +def _fix_mapslots(filetree: IFileTree, roottree: IFileTree) -> None: + for child in filetree: + if child.isDir() and isinstance(child, IFileTree): + _fix_mapslots(child, roottree) + elif _mapslot_pattern.search(child.name()): + child.moveTo(_get_slotstree(roottree)) + + +class Borderlands1ModDataChecker(BasicModDataChecker): + def dataLooksValid(self, filetree: IFileTree) -> ModDataChecker.CheckReturn: + parent = filetree.parent() + if parent is not None: + return self.dataLooksValid(parent) + + status = ModDataChecker.VALID + + nest = _get_nest(filetree) + if nest is not None: + status = ModDataChecker.FIXABLE + filetree = nest + + if _check_filetree(filetree, False) is ModDataChecker.INVALID: + return ModDataChecker.INVALID + + slotstree = filetree.find(_slots_path) + if slotstree is not None and not slotstree.isDir(): + return ModDataChecker.INVALID + + for child in filetree: + if child.isDir(): + destination = _mod_dirs.get(child.name().casefold()) + if destination is None: + child_status = _check_filetree(child, True) + if child_status is ModDataChecker.INVALID: + return ModDataChecker.INVALID + elif child_status is ModDataChecker.FIXABLE: + status = ModDataChecker.FIXABLE + elif destination != "/": + status = ModDataChecker.FIXABLE + elif _mapslot_pattern.search(child.name()): + status = ModDataChecker.FIXABLE + elif _extention_pattern.search(child.name()): + return ModDataChecker.INVALID + return status + + def fix(self, filetree: IFileTree) -> IFileTree: + nest = _get_nest(filetree) + if nest is not None: + conflict: FileTreeEntry | None = None + for child in tuple(nest): + if not child.moveTo(filetree): + conflict = child + conflict.detach() + filetree.remove(nest) + if conflict is not None: + conflict.moveTo(filetree) + + for child in tuple(filetree): + if child.isDir() and isinstance(child, IFileTree): + destination = _mod_dirs.get(child.name().casefold()) + if destination is None: + _fix_mapslots(child, filetree) + elif destination != "/": + filetree.move(child, destination) + elif _mapslot_pattern.search(child.name()): + child.moveTo(_get_slotstree(filetree)) + + return filetree + + +class Borderlands1Game(BasicGame): + Name = "Borderlands 1 Support Plugin" + Author = "Miner Of Worlds, RedxYeti, mopioid" + Version = "1.0.0" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(Borderlands1ModDataChecker()) + return True + + GameName = "Borderlands" + GameShortName = "Borderlands" + GameNexusName = "Borderlands GOTY" + GameSteamId = 8980 + GameBinary = "Binaries/Borderlands.exe" + GameDataPath = "." + GameSaveExtension = "sav" + GameDocumentsDirectory = "%DOCUMENTS%/My Games/Borderlands/" + GameSavesDirectory = "%GAME_DOCUMENTS%/savedata" + GameIniFiles = "%GAME_DOCUMENTS%/WillowGame/Config" diff --git a/libs/basic_games/games/game_control.py b/libs/basic_games/games/game_control.py new file mode 100644 index 0000000..2662410 --- /dev/null +++ b/libs/basic_games/games/game_control.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class ControlGame(BasicGame): + Name = "Control Support Plugin" + Author = "Zash" + Version = "1.0.0" + + GameName = "Control" + GameShortName = "control" + GameNexusId = 2936 + GameSteamId = 870780 + GameGogId = 2049187585 + GameEpicId = "calluna" + GameBinary = "Control.exe" + GameDataPath = "" + + def executables(self): + return [ + mobase.ExecutableInfo( + "Control (Launcher)", + QFileInfo( + self.gameDirectory(), + self.binaryName(), + ), + ), + mobase.ExecutableInfo( + "Control DX11", + QFileInfo( + self.gameDirectory(), + "Control_DX11.exe", + ), + ), + mobase.ExecutableInfo( + "Control DX12", + QFileInfo( + self.gameDirectory(), + "Control_DX12.exe", + ), + ), + ] + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + + libraries = ["iphlpapi.dll", "xinput1_4.dll"] + efls.extend( + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libraries + for exe in self.executables() + ) + return efls diff --git a/libs/basic_games/games/game_cyberpunk2077.py b/libs/basic_games/games/game_cyberpunk2077.py new file mode 100644 index 0000000..ab44b8b --- /dev/null +++ b/libs/basic_games/games/game_cyberpunk2077.py @@ -0,0 +1,798 @@ +import filecmp +import json +import re +import shutil +import tempfile +import textwrap +from collections import Counter +from collections.abc import Iterable +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Literal, TypeVar + +from PyQt6.QtCore import QDateTime, QDir, Qt, qCritical, qInfo, qWarning +from PyQt6.QtWidgets import ( + QCheckBox, + QMainWindow, + QMessageBox, + QProgressDialog, + QWidget, +) + +import mobase + +from ..basic_features import BasicLocalSavegames, BasicModDataChecker, GlobPatterns +from ..basic_features.basic_save_game_info import ( + BasicGameSaveGame, + BasicGameSaveGameInfo, + format_date, +) +from ..basic_game import BasicGame + + +class CyberpunkModDataChecker(BasicModDataChecker): + def __init__(self): + super().__init__( + GlobPatterns( + delete=[ + "*.gif", + "*.jpg", + "*.jpeg", + "*.jxl", + "*.md", + "*.png", + "*.txt", + "*.webp", + ], + move={ + # archive and ArchiveXL + "*.archive": "archive/pc/mod/", + "*.xl": "archive/pc/mod/", + }, + valid=[ + "archive", + # redscript + "engine", + "r6", + "mods", # RedMod + "red4ext", + "bin", # CET etc. gets handled below + ], + ) + ) + + +def time_from_seconds(s: int | float) -> str: + m, s = divmod(int(s), 60) + h, m = divmod(int(m), 60) + return f"{h:02}:{m:02}:{s:02}" + + +def parse_cyberpunk_save_metadata(save_path: Path, save: mobase.ISaveGame): + metadata_file = save_path / "metadata.9.json" + try: + with open(metadata_file) as file: + meta_data = json.load(file)["Data"]["metadata"] + name = meta_data["name"] + if name != (save_name := save.getName()): + name = f"{save_name} ({name})" + return { + "Name": name, + "Date": format_date(meta_data["timestampString"], "hh:mm:ss, d.M.yyyy"), + "Play Time": time_from_seconds(meta_data["playthroughTime"]), + "Quest": meta_data["trackedQuestEntry"], + "Level": int(meta_data["level"]), + "Street Cred": int(meta_data["streetCred"]), + "Life Path": meta_data["lifePath"], + "Difficulty": meta_data["difficulty"], + "Gender": f"{meta_data['bodyGender']} / {meta_data['brainGender']}", + "Game version": meta_data["buildPatch"], + } + except (FileNotFoundError, json.JSONDecodeError): + return None + + +class CyberpunkSaveGame(BasicGameSaveGame): + _name_file = "NamedSave.txt" # from mod: Named Saves + + def __init__(self, filepath: Path): + super().__init__(filepath) + try: # Custom name from Named Saves + with open(filepath / self._name_file) as file: + self._name = file.readline() + except FileNotFoundError: + self._name = "" + + def getName(self) -> str: + return self._name or super().getName() + + def getCreationTime(self) -> QDateTime: + return QDateTime.fromSecsSinceEpoch( + int((self._filepath / "sav.dat").stat().st_mtime) + ) + + +@dataclass +class ModListFile: + list_path: Path + mod_search_pattern: str + reversed_priority: bool = False + """True: load order priority is reversed compared to MO (first mod has priority).""" + + +_MOD_TYPE = TypeVar("_MOD_TYPE") + + +class ModListFileManager(dict[_MOD_TYPE, ModListFile]): + """Manages modlist files for specific mod types.""" + + def __init__(self, organizer: mobase.IOrganizer, **kwargs: ModListFile) -> None: + self._organizer = organizer + super().__init__(**kwargs) + + def update_modlist( + self, mod_type: _MOD_TYPE, mod_files: list[str] | None = None + ) -> tuple[Path, list[str], list[str]]: + """ + Updates the mod list file for `mod_type` with the current load order. + Removes the file if it is not needed. + + Args: + mod_type: Which modlist to update. + mod_files (optional): By default mod files in order of `mod_type` priority + (respecting `self[mod_type].reversed_priority`). + + Returns: + `(modlist_path, new_mod_list, old_mod_list)` + """ + if mod_files is None: + mod_files = list(self.modfile_names(mod_type)) + modlist_path = self.absolute_modlist_path(mod_type) + old_modlist = ( + modlist_path.read_text().splitlines() if modlist_path.exists() else [] + ) + if not mod_files or len(mod_files) == 1: + # No load order required + if old_modlist: + qInfo(f"Removing {mod_type} load order {modlist_path}") + modlist_path.unlink() + return modlist_path, [], old_modlist + else: + qInfo(f'Updating {mod_type} load order "{modlist_path}" with: {mod_files}') + modlist_path.parent.mkdir(parents=True, exist_ok=True) + modlist_path.write_text("\n".join(mod_files)) + return modlist_path, mod_files, old_modlist + + def absolute_modlist_path(self, mod_type: _MOD_TYPE) -> Path: + modlist_path = self[mod_type].list_path + if not modlist_path.is_absolute(): + existing = self._organizer.findFiles(modlist_path.parent, modlist_path.name) + overwrite = self._organizer.overwritePath() + modlist_path = ( + Path(existing[0]) if (existing) else Path(overwrite, modlist_path) + ) + return modlist_path + + def modfile_names(self, mod_type: _MOD_TYPE) -> Iterable[str]: + """Get all file names from the `mod_type` in MOs load order + (reversed with `self[mod_type].reversed_priority = True`). + """ + yield from (file.name for file in self.modfiles(mod_type)) + + def modfiles(self, mod_type: _MOD_TYPE) -> Iterable[Path]: + """Get all files from the `mod_type` in MOs load order + (reversed with `self[mod_type].reversed_priority = True`). + """ + mod_search_pattern = self[mod_type].mod_search_pattern + for mod_path in self.active_mod_paths(self[mod_type].reversed_priority): + yield from mod_path.glob(mod_search_pattern) + + def active_mod_paths(self, reverse: bool = False) -> Iterable[Path]: + """Yield the path to active mods in MOs load order.""" + mods_path = Path(self._organizer.modsPath()) + modlist = self._organizer.modList() + mods_load_order = modlist.allModsByProfilePriority() + for mod in reversed(mods_load_order) if reverse else mods_load_order: + if modlist.state(mod) & mobase.ModState.ACTIVE: + yield mods_path / mod + + +class Cyberpunk2077Game(BasicGame): + Name = "Cyberpunk 2077 Support Plugin" + Author = "6788, Zash" + Version = "3.0.1" + + GameName = "Cyberpunk 2077" + GameShortName = "cyberpunk2077" + GameBinary = "bin/x64/Cyberpunk2077.exe" + GameLauncher = "REDprelauncher.exe" + GameDataPath = "%GAME_PATH%" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/CD Projekt Red/Cyberpunk 2077" + GameSavesDirectory = "%USERPROFILE%/Saved Games/CD Projekt Red/Cyberpunk 2077" + GameSaveExtension = "dat" + GameSteamId = 1091500 + GameGogId = 1423049311 + GameEpicId = "77f2b98e2cef40c8a7437518bf420e47" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Cyberpunk-2077" + ) + + # CET and RED4ext, relative to Cyberpunk2077.exe + _forced_libraries = ["version.dll", "winmm.dll"] + _crashreporter_path = "bin/x64/CrashReporter/CrashReporter.exe" + + _redmod_binary = Path("tools/redmod/bin/redMod.exe") + _redmod_log = Path("tools/redmod/bin/REDmodLog.txt") + _redmod_deploy_path = Path("r6/cache/modded/") + _redmod_deploy_args = "deploy -reportProgress" + """Deploy arguments for `redmod.exe`, -modlist=... is added.""" + + _parentWidget: QWidget + """Set with `_organizer.onUserInterfaceInitialized()`""" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(BasicLocalSavegames(self)) + self._register_feature( + BasicGameSaveGameInfo( + lambda p: Path(p or "", "screenshot.png"), + parse_cyberpunk_save_metadata, + ) + ) + self._register_feature(CyberpunkModDataChecker()) + + self._modlist_files = ModListFileManager[Literal["archive", "redmod"]]( + organizer, + archive=ModListFile( + Path("archive/pc/mod/modlist.txt"), + "archive/pc/mod/*.archive", + reversed_priority=bool(self._get_setting("reverse_archive_load_order")), + ), + redmod=ModListFile( + Path(self._redmod_deploy_path, "MO_REDmod_load_order.txt"), + "mods/*/", + reversed_priority=bool(self._get_setting("reverse_redmod_load_order")), + ), + ) + organizer.onAboutToRun(self._onAboutToRun) + organizer.onFinishedRun(self._onFinishedRun) + organizer.onPluginSettingChanged(self._on_settings_changed) + organizer.modList().onModInstalled(self._check_disable_crashreporter) + organizer.onUserInterfaceInitialized(self._on_user_interface_initialized) + return True + + def _on_settings_changed( + self, + plugin_name: str, + setting: str, + old: mobase.MoVariant, + new: mobase.MoVariant, + ): + if self.name() != plugin_name: + return + match setting: + case "reverse_archive_load_order": + self._modlist_files["archive"].reversed_priority = bool(new) + case "reverse_remod_load_order": + self._modlist_files["redmod"].reversed_priority = bool(new) + case "show_rootbuilder_conversion": + if new and (dialog := self._get_rootbuilder_conversion_dialog()): + dialog.open() # type: ignore + case _: + return + + def _on_user_interface_initialized(self, window: QMainWindow): + self._parentWidget = window + if not self.isActive(): + return + if dialog := self._get_rootbuilder_conversion_dialog(window): + dialog.open() # type: ignore + else: + self._check_disable_crashreporter() + + def _check_disable_crashreporter( + self, mod: mobase.IModInterface | None | Any = None + ): + """ + Disable Crashreporter with CET installed in VFS, since it crashes + when trying to resolve `version.dll`. + """ + if not self.isActive() or not self._get_setting("disable_crashreporter"): + return + cet_mod_name = self._find_cet_mod_name( + mod if isinstance(mod, mobase.IModInterface) else None + ) + if ( + cet_mod_name + and (cr_origin := self._organizer.getFileOrigins(self._crashreporter_path)) + and cr_origin[0] == "data" + ): + self._create_dummy_crashreporter_mod( + self._organizer.modList().priority(cet_mod_name) + 1 + ) + + def _find_cet_mod_name(self, mod: mobase.IModInterface | None = None) -> str: + """ + Find the mod containing `version.dll`. + + Args: + mod (optional): check the mods filetree instead. Defaults to None. + + Returns: + The mods name if `version.dll` is found, else ''. + """ + cet_mod_name = "" + if mod: + if mod.fileTree().find("bin/x64/version.dll"): + cet_mod_name = mod.name() + elif dll_origins := self._organizer.getFileOrigins("bin/x64/version.dll"): + cet_mod_name = dll_origins[0] + return cet_mod_name if cet_mod_name != "data" else "" + + def _create_dummy_crashreporter_mod(self, priority: int = 0): + """ + Disables CrashReporter by creating an empty dummy file to replace + `CrashReporter.exe`. + """ + mod_name = "disable CrashReporter (MO CET fix)" + modlist = self._organizer.modList() + if modlist.getMod(mod_name): + return + qInfo(f"CET VFS fix: creating mod {mod_name}") + new_mod = self._organizer.createMod(mobase.GuessedString(mod_name)) + if not new_mod: + return + mod_name = new_mod.name() + new_mod.setGameName(self.gameShortName()) + new_mod.setUrl(f"{self.getSupportURL()}#crashreporter") + crashReporter = Path(new_mod.absolutePath(), self._crashreporter_path) + crashReporter.parent.mkdir(parents=True) + crashReporter.touch() + + def callback(): + modlist.setActive(mod_name, True) + modlist.setPriority(mod_name, priority) + + self._organizer.onNextRefresh(callback, False) + self._organizer.refresh() + self._set_setting("disable_crashreporter", False) + + def iniFiles(self): + return ["UserSettings.json"] + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + CyberpunkSaveGame(path.parent) + for path in Path(folder.absolutePath()).glob(f"**/*.{ext}") + ] + + def settings(self) -> list[mobase.PluginSetting]: + return [ + mobase.PluginSetting( + "skipStartScreen", + ( + 'Skips the "Breaching..." start screen on game launch' + " (can also skip loading of GOG rewards)" + ), + False, + ), + mobase.PluginSetting( + "enforce_archive_load_order", + ( + "Enforce the current load order via" + " archive/pc/mod/modlist.txt" + ), + False, + ), + mobase.PluginSetting( + "reverse_archive_load_order", + ( + "Reverse MOs load order in" + " archive/pc/mod/modlist.txt" + " (first loaded mod wins = last one / highest prio in MO)" + ), + False, + ), + mobase.PluginSetting( + "enforce_redmod_load_order", + "Enforce the current load order on redmod deployment", + True, + ), + mobase.PluginSetting( + "reverse_redmod_load_order", + ( + "Reverse MOs load order on redmod deployment" + " (first loaded mod wins = last one / highest prio in MO)" + ), + False, + ), + mobase.PluginSetting( + "auto_deploy_redmod", + "Deploy redmod before game launch if necessary", + True, + ), + mobase.PluginSetting( + "clear_cache_after_game_update", + ( + 'Clears "overwrite/r6/cache/*" if the original game files changed' + " (after update)" + ), + True, + ), + mobase.PluginSetting( + "disable_crashreporter", + ( + "Creates a dummy mod/file to disable CrashReporter.exe, " + "which is not compatible with VFS version.dll, see wiki" + ), + True, + ), + mobase.PluginSetting( + "crash_message", + ("Show a crash message as replacement of disabled CrashReporter"), + True, + ), + mobase.PluginSetting( + "show_rootbuilder_conversion", + ( + "Shows a dialog to convert legacy RootBuilder mods to native MO mods," + " using force load libraries" + ), + True, + ), + ] + + def _get_setting(self, key: str) -> mobase.MoVariant: + return self._organizer.pluginSetting(self.name(), key) + + def _set_setting(self, key: str, value: mobase.MoVariant): + self._organizer.setPluginSetting(self.name(), key, value) + + def executables(self) -> list[mobase.ExecutableInfo]: + game_name = self.gameName() + game_dir = self.gameDirectory() + bin_path = game_dir.absoluteFilePath(self.binaryName()) + skip_start_screen = ( + "-skipStartScreen" if self._get_setting("skipStartScreen") else "" + ) + return [ + # Default, runs REDmod deploy if necessary + mobase.ExecutableInfo( + f"{game_name} (REDmod)", + bin_path, + ).withArgument(f"--launcher-skip -modded {skip_start_screen}"), + # Start game without REDmod + mobase.ExecutableInfo( + f"{game_name}", + bin_path, + ).withArgument(f"--launcher-skip {skip_start_screen}"), + # Deploy REDmods only + mobase.ExecutableInfo( + "REDmod", + self._get_redmod_binary(), + ).withArgument("deploy -reportProgress -force %modlist%"), + # Launcher + mobase.ExecutableInfo( + "REDprelauncher", + game_dir.absoluteFilePath(self.getLauncherName()), + ).withArgument(f"{skip_start_screen}"), + ] + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + exe = Path(self.binaryName()).name + return [ + mobase.ExecutableForcedLoadSetting(exe, lib).withEnabled(True) + for lib in self._forced_libraries + ] + + def _get_redmod_binary(self) -> Path: + """Absolute path to redmod binary""" + return Path(self.gameDirectory().absolutePath(), self._redmod_binary) + + def _onAboutToRun(self, app_path_str: str, wd: QDir, args: str) -> bool: + if not self.isActive(): + return True + app_path = Path(app_path_str) + if app_path == self._get_redmod_binary(): + if m := re.search(r"%modlist%", args, re.I): + # Manual deployment: replace %modlist% variable + ( + modlist_path, + modlist, + _, + ) = self._modlist_files.update_modlist("redmod") + modlist_param = f'-modlist="{modlist_path}"' if modlist else "" + args = f"{args[: m.start()]}{modlist_param}{args[m.end() :]}" + qInfo(f"Manual modlist deployment: replacing {m[0]}, new args = {args}") + self._check_redmod_result( + self._organizer.waitForApplication( + self._organizer.startApplication(app_path_str, [args], wd), + False, + ) + ) + return False # redmod with new args started + return True # No recursive redmod call + if ( + self._get_setting("auto_deploy_redmod") + and app_path == Path(self.gameDirectory().absolutePath(), self.binaryName()) + and "-modded" in args + and not self._check_redmod_result(self._deploy_redmod()) + ): + qWarning("Aborting game launch.") + return False # Auto deploy failed + self._map_cache_files() + if self._get_setting("enforce_archive_load_order"): + self._modlist_files.update_modlist("archive") + return True + + def _onFinishedRun(self, path: str, exit_code: int) -> None: + if not self._get_setting("crash_message"): + return + if path.endswith(self.binaryName()) and exit_code > 0: + crash_message = QMessageBox( + QMessageBox.Icon.Critical, + "Cyberpunk Crashed", + textwrap.dedent( + f""" + Cyberpunk crashed. Tips: + - disable mods (create backup of modlist or use new profile) + - clear overwrite or delete at least overwrite/r6/cache (to keep mod settings) + - check log files of CET/redscript/RED4ext (in overwrite) + - read [FAQ & Troubleshooting]({self.GameSupportURL}#faq--troubleshooting) + """ + ), + QMessageBox.StandardButton.Ok, + self._parentWidget, + ) + crash_message.setTextFormat(Qt.TextFormat.MarkdownText) + hide_cb = QCheckBox("&Do not show again*", crash_message) + hide_cb.setToolTip(f"Settings/Plugins/{self.name()}/crash_message") + crash_message.setCheckBox(hide_cb) + crash_message.open( # type: ignore + lambda: hide_cb.isChecked() + and self._set_setting("crash_message", False) + ) + + def _check_redmod_result(self, result: tuple[bool, int]) -> bool: + if result == (True, 0): + return True + if result[1] < 0: + qWarning(f"REDmod deployment aborted (exit code {result[1]}).") + else: + qCritical( + f"REDmod deployment failed with exit code {result[1]} !" + f" Check {Path('GAME_FOLDER/', self._redmod_log)}" + ) + return False + + def _deploy_redmod(self) -> tuple[bool, int]: + """Deploys redmod. Clears deployed files if no redmods are active. + Recreates deployed files to force load order when necessary. + + Returns: + (success?, exit code) + """ + # Add REDmod load order if none is specified + redmod_list = list(self._modlist_files.modfile_names("redmod")) + if not redmod_list: + qInfo("Cleaning up redmod deployed files") + self._clean_deployed_redmod() + return True, 0 + args = self._redmod_deploy_args + if self._get_setting("enforce_redmod_load_order"): + modlist_path, _, old_redmods = self._modlist_files.update_modlist( + "redmod", redmod_list + ) + if ( + Counter(redmod_list) == Counter(old_redmods) + and not redmod_list == old_redmods + ): + # Only load order changed: recreate redmod deploys + # Fix for redmod not detecting change of load order. + # Faster than -force https://github.com/E1337Kat/cyberpunk2077_ext_redux/issues/297 # noqa: E501 + qInfo("Redmod order changed, recreate deployed files") + self._clean_deployed_redmod(modlist_path) + qInfo(f"Deploying redmod with modlist: {modlist_path}") + args += f' -modlist="{modlist_path}"' + else: + qInfo("Deploying redmod") + redmod_binary = self._get_redmod_binary() + return self._organizer.waitForApplication( + self._organizer.startApplication( + redmod_binary, [args], redmod_binary.parent + ), + False, + ) + + def _clean_deployed_redmod(self, modlist_path: Path | None = None): + """Delete all files from `_redmod_deploy_path` except for `modlist_path`.""" + for file in self._organizer.findFiles(self._redmod_deploy_path, "*"): + file_path = Path(file) + if modlist_path is None or file_path != modlist_path: + file_path.unlink() + + def _map_cache_files(self): + """ + Copy cache files (`final.redscript` etc.) to overwrite to catch + overwritten game files. + """ + data_path = Path(self.dataDirectory().absolutePath()) + overwrite_path = Path(self._organizer.overwritePath()) + data_cache_path = data_path / "r6/cache" + overwrite_cache_path = overwrite_path / "r6/cache" + cache_files = ( + file.relative_to(data_path) for file in data_cache_path.glob("*") + ) + if self._get_setting("clear_cache_after_game_update") and any( + self._is_cache_file_updated(file, data_path) for file in cache_files + ): + qInfo('Updated game files detected, clearing "overwrite/r6/cache/*"') + try: + shutil.rmtree(overwrite_cache_path) + except FileNotFoundError: + pass + qInfo('Updating "r6/cache" in overwrite') + shutil.copytree(data_cache_path, overwrite_cache_path, dirs_exist_ok=True) + else: + for file in self._unmapped_cache_files(data_path): + qInfo(f'Copying "{file}" to overwrite (to catch file overwrites)') + dst = overwrite_path / file + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(data_path / file, dst) + + def _is_cache_file_updated(self, file: Path, data_path: Path) -> bool: + """Checks if a cache file is updated (in game dir). + + Args: + file: Relative to data dir. + """ + game_file = data_path.absolute() / file + mapped_files = self._organizer.findFiles(file.parent, file.name) + return bool( + mapped_files + and (mapped_file := mapped_files[0]) + and not ( + game_file.samefile(mapped_file) + or filecmp.cmp(game_file, mapped_file) + or ( # different backup file + ( + backup_files := self._organizer.findFiles( + file.parent, f"{file.name}.bk" + ) + ) + and filecmp.cmp(game_file, backup_files[0]) + ) + ) + ) + + def _unmapped_cache_files(self, data_path: Path) -> Iterable[Path]: + """Yields unmapped cache files relative to `data_path`.""" + for file in self._organizer.findFiles("r6/cache", "*"): + try: + yield Path(file).absolute().relative_to(data_path) + except ValueError: + continue + + def _get_rootbuilder_conversion_dialog( + self, parent_widget: QWidget | None = None + ) -> QMessageBox | None: + """ + Dialog to convert any mods with `root` folder, if applicable. + CET and RED4ext work with forced load libraries since ~ Cyberpunk v2.12, + making RootBuilder unnecessary. + """ + setting = "show_rootbuilder_conversion" + if not self.isActive() or not self._get_setting(setting): + return None + if not ( + (root_folder := self._organizer.virtualFileTree().find("root")) + and root_folder.isDir() + ): + return None + parent_widget = parent_widget or self._parentWidget + message_box = QMessageBox( + QMessageBox.Icon.Question, + "RootBuilder obsolete", + textwrap.dedent( + """ + Mod Organizer now supports Cyberpunk Engine Tweaks (CET) and + RED4ext native via forced load libraries, making RootBuilder + unnecessary. + + Do you want to convert all mods with a `root` folder now? +
This usually only affects CET, RED4ext and overwrite. + + You can disable RootBuilder afterwards. + """ + ), + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + parent_widget, + ) + message_box.setTextFormat(Qt.TextFormat.MarkdownText) + checkbox = QCheckBox("&Do not show again*", parent_widget) + checkbox.setChecked(True) + checkbox.setToolTip(f"Settings/Plugins/{self.name()}/{setting}") + message_box.setCheckBox(checkbox) + + def accept_callback(): + if unfolded_mods := unfold_root_folders(self._organizer, parent_widget): + n_mods = len(unfolded_mods) + info = QMessageBox( + QMessageBox.Icon.Information, + "Root mods converted", + ( + f"{n_mods} mod{'s' if n_mods > 1 else ''} converted." + "You can disable RootBuilder in the settings now." + ), + parent=parent_widget, + ) + info.setDetailedText(f"Converted mods:\n{'\n'.join(unfolded_mods)}") + info.open() # type: ignore + + message_box.accepted.connect(accept_callback) # type: ignore + + def finished_callback(): + if checkbox.isChecked(): + self._set_setting(setting, False) + self._check_disable_crashreporter() + + message_box.finished.connect(finished_callback) # type: ignore + return message_box + + +def unfold_root_folders( + organizer: mobase.IOrganizer, parent_widget: QWidget | None = None +) -> list[str]: + """Unfolds (RootBuilders) root folders of all mods (excluding backups).""" + mods = organizer.modList().allMods() + progress = None + unfolded_mods: list[str] = [] + if parent_widget: + progress = QProgressDialog( + "Merging/unfolding root folders...", + "Abort", + 0, + len(mods), + parent_widget, + ) + progress.setWindowModality(Qt.WindowModality.WindowModal) + for i, mod_name in enumerate(mods): + if progress: + if progress.wasCanceled(): + break + progress.setValue(i) + if mod_name == "data": + continue + mod = organizer.modList().getMod(mod_name) + if mod.isBackup() or mod.isSeparator() or mod.isForeign(): + continue + root_folder = mod.fileTree().find("root") + if root_folder is None or not root_folder.isDir(): + continue + qInfo(f"Merging root folder of {mod_name}") + mod_path = Path(mod.absolutePath()) + root_folder_path = mod_path / "root" + unfold_folder(root_folder_path) + unfolded_mods.append(mod_name) + if progress: + progress.setValue(len(mods)) + organizer.refresh() + return unfolded_mods + + +def unfold_folder(src_path: Path): + """ + Unfolds a folder (`parent/src/* -> parent/*`), overwriting existing files/folders. + Preserves a subfolder with same name (`parent/src/src -> parent/src`). + """ + parent = src_path.parent + if (src_path / src_path.name).exists(): + # Contains a file/folder with same name + with tempfile.TemporaryDirectory(dir=parent) as temp_folder: + src_path = src_path.rename(parent / temp_folder / src_path.name) + shutil.copytree(src_path, parent, symlinks=True, dirs_exist_ok=True) + else: + shutil.copytree(src_path, parent, symlinks=True, dirs_exist_ok=True) + shutil.rmtree(src_path) diff --git a/libs/basic_games/games/game_da2.py b/libs/basic_games/games/game_da2.py new file mode 100644 index 0000000..9d481e7 --- /dev/null +++ b/libs/basic_games/games/game_da2.py @@ -0,0 +1,35 @@ +import mobase + +from ..basic_features import BasicGameSaveGameInfo +from ..basic_game import BasicGame + + +class DA2Game(BasicGame): + Name = "Dragon Age 2 Support Plugin" + Author = "Patchier" + + GameName = "Dragon Age 2" + GameShortName = "dragonage2" + GameBinary = r"bin_ship\DragonAge2.exe" + GameDataPath = r"%DOCUMENTS%\BioWare\Dragon Age 2\packages\core\override" + GameSavesDirectory = r"%DOCUMENTS%\BioWare\Dragon Age 2\Characters" + GameSaveExtension = "das" + GameSteamId = 1238040 + GameOriginManifestIds = ["OFB-EAST:59474", "DR:201797000"] + GameOriginWatcherExecutables = ("DragonAge2.exe",) + GameEaDesktopId = [70784, 1002980] + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Dragon-Age-II" + ) + + def version(self): + # Don't forget to import mobase! + return mobase.VersionInfo(1, 0, 1, mobase.ReleaseType.FINAL) + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._register_feature( + BasicGameSaveGameInfo(lambda s: s.parent.joinpath("screen.dds")) + ) + return True diff --git a/libs/basic_games/games/game_daggerfallunity.py b/libs/basic_games/games/game_daggerfallunity.py new file mode 100644 index 0000000..6c170f1 --- /dev/null +++ b/libs/basic_games/games/game_daggerfallunity.py @@ -0,0 +1,56 @@ +import mobase + +from ..basic_game import BasicGame + + +class DaggerfallUnityModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + self.validDirNames = [ + "biogs", + "docs", + "factions", + "fonts", + "mods", + "questpacks", + "quests", + "sound", + "soundfonts", + "spellicons", + "tables", + "text", + "textures", + "worlddata", + "aa", + ] + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + for entry in filetree: + if not entry.isDir(): + continue + if entry.name().casefold() in self.validDirNames: + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.INVALID + + +class DaggerfallUnityGame(BasicGame): + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(DaggerfallUnityModDataChecker()) + return True + + Name = "Daggerfall Unity Support Plugin" + Author = "HomerSimpleton" + Version = "1.0.0" + + GameName = "Daggerfall Unity" + GameShortName = "daggerfallunity" + GameBinary = "DaggerfallUnity.exe" + GameLauncher = "DaggerfallUnity.exe" + GameDataPath = "%GAME_PATH%/DaggerfallUnity_Data/StreamingAssets" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Daggerfall-Unity" + ) diff --git a/libs/basic_games/games/game_dao.py b/libs/basic_games/games/game_dao.py new file mode 100644 index 0000000..bc3f87e --- /dev/null +++ b/libs/basic_games/games/game_dao.py @@ -0,0 +1,31 @@ +import mobase + +from ..basic_features import BasicGameSaveGameInfo +from ..basic_game import BasicGame + + +class DAOriginsGame(BasicGame): + Name = "Dragon Age Origins Support Plugin" + Author = "Patchier" + Version = "1.1.1" + + GameName = "Dragon Age: Origins" + GameShortName = "dragonage" + GameBinary = r"bin_ship\DAOrigins.exe" + GameDataPath = r"%DOCUMENTS%\BioWare\Dragon Age\packages\core\override" + GameSavesDirectory = r"%DOCUMENTS%\BioWare\Dragon Age\Characters" + GameSaveExtension = "das" + GameSteamId = [17450, 47810] + GameGogId = 1949616134 + GameEaDesktopId = [70377, 70843] + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Dragon-Age:-Origins" + ) + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._register_feature( + BasicGameSaveGameInfo(lambda s: s.parent.joinpath("screen.dds")) + ) + return True diff --git a/libs/basic_games/games/game_darkestdungeon.py b/libs/basic_games/games/game_darkestdungeon.py new file mode 100644 index 0000000..5c3ae4e --- /dev/null +++ b/libs/basic_games/games/game_darkestdungeon.py @@ -0,0 +1,245 @@ +import json +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_game import BasicGame, BasicGameSaveGame +from ..steam_utils import find_steam_path + + +class DarkestDungeonModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + self.validDirNames = [ + "activity_log", + "audio", + "campaign", + "colours", + "curios", + "cursors", + "dlc", + "dungeons", + "effects", + "fe_flow", + "fonts", + "fx", + "game_over", + "heroes", + "inventory", + "loading_screen", + "localization", + "loot", + "maps", + "modes", + "monsters", + "overlays", + "panels", + "props", + "raid", + "raid_result", + "scripts", + "scrolls", + "shaders", + "shared", + "trinkets", + "upgrades", + "video", + ] + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + for entry in filetree: + if not entry.isDir(): + continue + if entry.name().casefold() in self.validDirNames: + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.INVALID + + +class DarkestDungeonSaveGame(BasicGameSaveGame): + def __init__(self, filepath: Path): + super().__init__(filepath) + dataPath = filepath.joinpath("persist.game.json") + self.name: str = "" + if self.isBinary(dataPath): + self.loadBinarySaveFile(dataPath) + else: + self.loadJSONSaveFile(dataPath) + + @staticmethod + def isBinary(dataPath: Path) -> bool: + with dataPath.open(mode="rb") as fp: + magic = fp.read(4) + # magic number in binary save files + return magic == b"\x01\xb1\x00\x00" + + def loadJSONSaveFile(self, dataPath: Path): + text = dataPath.read_text() + content = json.loads(text) + data = content["data"] + self.name = str(data["estatename"]) + + def loadBinarySaveFile(self, dataPath: Path): + # see https://github.com/robojumper/DarkestDungeonSaveEditor + with dataPath.open(mode="rb") as fp: + # read Header + + # skip to headerLength + fp.seek(8, 0) + headerLength = int.from_bytes(fp.read(4), "little") + if headerLength != 64: + raise ValueError("Header Length is not 64: " + str(headerLength)) + fp.seek(4, 1) + + # meta1Size = int.from_bytes(fp.read(4), "little") + fp.seek(4, 1) + # numMeta1Entries = int.from_bytes(fp.read(4), "little") + fp.seek(4, 1) + + meta1Offset = int.from_bytes(fp.read(4), "little") + fp.seek(16, 1) + numMeta2Entries = int.from_bytes(fp.read(4), "little") + meta2Offset = int.from_bytes(fp.read(4), "little") + fp.seek(4, 1) + + # dataLength = int.from_bytes(fp.read(4), "little") + fp.seek(4, 1) + + dataOffset = int.from_bytes(fp.read(4), "little") + + # read Meta1 Block + fp.seek(meta1Offset, 0) + meta1DataLength = meta2Offset - meta1Offset + if meta1DataLength % 16 != 0: + raise ValueError( + "Meta1 has wrong number of bytes: " + str(meta1DataLength) + ) + + # read Meta2 Block + fp.seek(meta2Offset, 0) + meta2DataLength = dataOffset - meta2Offset + if meta2DataLength % 12 != 0: + raise ValueError( + "Meta2 has wrong number of bytes: " + str(meta2DataLength) + ) + meta2List: list[tuple[int, int, int]] = [] + for _ in range(numMeta2Entries): + entryHash = int.from_bytes(fp.read(4), "little") + offset = int.from_bytes(fp.read(4), "little") + fieldInfo = int.from_bytes(fp.read(4), "little") + meta2List.append((entryHash, offset, fieldInfo)) + + # read Data + fp.seek(dataOffset, 0) + for x in range(numMeta2Entries): + meta2Entry = meta2List[x] + fp.seek(dataOffset + meta2Entry[1], 0) + nameLength = (meta2Entry[2] & 0b11111111100) >> 2 + # null terminated string + nameBytes = fp.read(nameLength - 1) + fp.seek(1, 1) + name = bytes.decode(nameBytes, "utf-8") + if name != "estatename": + continue + valueLength = int.from_bytes(fp.read(4), "little") + valueBytes = fp.read(valueLength - 1) + value = bytes.decode(valueBytes, "utf-8") + self.name = value + break + + def getName(self) -> str: + if self.name == "": + return super().getName() + return self.name + + +class DarkestDungeonGame(BasicGame): + Name = "DarkestDungeon" + Author = "erri120" + Version = "0.2.0" + + GameName = "Darkest Dungeon" + GameShortName = "darkestdungeon" + GameNexusName = "darkestdungeon" + GameNexusId = 804 + GameSteamId = 262060 + GameGogId = 1719198803 + GameBinary = "_windows/win64/Darkest.exe" + GameDataPath = "" + GameDocumentsDirectory = "%DOCUMENTS%/Darkest" + GameSavesDirectory = "%GAME_DOCUMENTS%" + GameSupportURL = "https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/Game:-Darkest-Dungeon" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(DarkestDungeonModDataChecker()) + return True + + def executables(self) -> list[mobase.ExecutableInfo]: + game_name = self.gameName() + game_dir = self.gameDirectory() + + return [ + mobase.ExecutableInfo( + f"{game_name} Steam x64", + QFileInfo(game_dir, "_windows/win64/Darkest.exe"), + ).withWorkingDirectory(game_dir), + mobase.ExecutableInfo( + f"{game_name} x64", + QFileInfo(game_dir, "_windowsnosteam/win64/Darkest.exe"), + ).withWorkingDirectory(game_dir), + mobase.ExecutableInfo( + f"{game_name} Steam x32", + QFileInfo(game_dir, "_windows/win32/Darkest.exe"), + ).withWorkingDirectory(game_dir), + mobase.ExecutableInfo( + f"{game_name} x32", + QFileInfo(game_dir, "_windowsnosteam/win32/Darkest.exe"), + ).withWorkingDirectory(game_dir), + ] + + @staticmethod + def getCloudSaveDirectory() -> str | None: + steamPath = find_steam_path() + if steamPath is None: + return None + + userData = steamPath.joinpath("userdata") + for child in userData.iterdir(): + name = child.name + try: + userID = int(name) + except ValueError: + userID = -1 + if userID == -1: + continue + cloudSaves = child.joinpath("262060", "remote") + if cloudSaves.exists() and cloudSaves.is_dir(): + return str(cloudSaves) + return None + + def savesDirectory(self) -> QDir: + documentsSaves = self.documentsDirectory() + + if not self.is_steam(): + return documentsSaves + + cloudSaves = self.getCloudSaveDirectory() + if cloudSaves is None: + return documentsSaves + + return QDir(cloudSaves) + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + profiles: list[Path] = [] + for path in Path(folder.absolutePath()).glob("profile_*"): + # profile_9 is only for the Multiplayer DLC "The Butcher's Circus" + # and contains different files than other profiles + if path.name == "profile_9": + continue + profiles.append(path) + + return [DarkestDungeonSaveGame(path) for path in profiles] diff --git a/libs/basic_games/games/game_darkmessiahofmightandmagic.py b/libs/basic_games/games/game_darkmessiahofmightandmagic.py new file mode 100644 index 0000000..ed3fbcd --- /dev/null +++ b/libs/basic_games/games/game_darkmessiahofmightandmagic.py @@ -0,0 +1,48 @@ +import struct +from pathlib import Path + +from PyQt6.QtGui import QImage + +import mobase + +from ..basic_features import BasicGameSaveGameInfo +from ..basic_game import BasicGame + + +class DarkMessiahOfMightAndMagicGame(BasicGame): + Name = "Dark Messiah of Might and Magic Support Plugin" + Author = "Holt59" + Version = "0.1.0" + + GameName = "Dark Messiah of Might & Magic" + GameShortName = "darkmessiahofmightandmagic" + GameNexusName = "darkmessiahofmightandmagic" + GameNexusId = 628 + GameSteamId = 2100 + GameBinary = "mm.exe" + GameDataPath = "mm" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Dark-Messiah-of-Might-&-Magic" + ) + + GameDocumentsDirectory = "%GAME_PATH%/mm" + GameSavesDirectory = "%GAME_PATH%/mm/SAVE" + GameSaveExtension = "sav" + + def _read_save_tga(self, filepath: Path) -> QImage | None: + # Qt TGA reader does not work for TGA, I hope that all files + # have the same format: + with open( + filepath.parent.joinpath(filepath.name.replace(".sav", ".tga")), "rb" + ) as fp: + data = fp.read() + _, _, w, h, bpp, _ = struct.unpack(" bool: + super().init(organizer) + self._register_feature(DispatchModDataChecker()) + self._register_feature(BasicLocalSavegames(self)) + return True + + def executables(self): + return [ + mobase.ExecutableInfo("Dispatch", self.GameBinary), + ] + + ## SAVE + # credit to game.darkestdungeon.py + @staticmethod + def getCloudSaveDirectory() -> str | None: + steamPath = find_steam_path() + if steamPath is None: + return None + + userData = steamPath.joinpath("userdata") + for child in userData.iterdir(): + name = child.name + try: + int(name) + except ValueError: + continue + + cloudSaves = child.joinpath("2592160/remote") + if cloudSaves.exists() and cloudSaves.is_dir(): + return str(cloudSaves) + return None + + def savesDirectory(self) -> QDir: + return QDir(self.getCloudSaveDirectory()) + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + saves: list[Path] = [] + for path in Path(folder.absolutePath()).glob("*.bin"): + saves.append(path) + + ##TODO: need a proper implementation + return [BasicGameSaveGame(path) for path in saves] + + ## MAPPING + + def exeDirectory(self) -> QDir: + return QDir(QDir(self.gameDirectory()).filePath("Dispatch/Binaries/Win64")) + + def mappings(self) -> list[mobase.Mapping]: + return [ + mobase.Mapping("*.dll", self.exeDirectory().absolutePath(), False, True), + ] diff --git a/libs/basic_games/games/game_divinityoriginalsin.py b/libs/basic_games/games/game_divinityoriginalsin.py new file mode 100644 index 0000000..435846e --- /dev/null +++ b/libs/basic_games/games/game_divinityoriginalsin.py @@ -0,0 +1,39 @@ +import mobase + +from ..basic_features import BasicGameSaveGameInfo +from ..basic_game import BasicGame + + +class DivinityOriginalSinGame(BasicGame): + Name = "Divinity: Original Sin (Classic) Support Plugin" + Author = "LostDragonist" + Version = "1.0.0" + + GameName = "Divinity: Original Sin (Classic)" + GameShortName = "divinityoriginalsin" + GameNexusName = "divinityoriginalsin" + GameValidShortNames = ["divinityoriginalsin"] + GameNexusId = 573 + GameSteamId = [230230] + GameBinary = "Shipping/EoCApp.exe" + GameDataPath = "Data" + GameSaveExtension = "lsv" # Not confirmed + GameDocumentsDirectory = ( + "%USERPROFILE%/Documents/Larian Studios/Divinity Original Sin" + ) + GameSavesDirectory = ( + "%USERPROFILE%/Documents/Larian Studios/Divinity Original Sin/PlayerProfiles" + ) + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Divinity:-Original-Sin" + ) + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._register_feature( + BasicGameSaveGameInfo( + lambda s: s.with_suffix(".png") # Not confirmed + ) + ) + return True diff --git a/libs/basic_games/games/game_divinityoriginalsinee.py b/libs/basic_games/games/game_divinityoriginalsinee.py new file mode 100644 index 0000000..fd3836f --- /dev/null +++ b/libs/basic_games/games/game_divinityoriginalsinee.py @@ -0,0 +1,119 @@ +# -*- encoding: utf-8 -*- + +import os +from pathlib import Path + +import mobase + +from ..basic_features import BasicGameSaveGameInfo +from ..basic_game import BasicGame + + +class DivinityOriginalSinEnhancedEditionModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + folders: list[mobase.IFileTree] = [] + files: list[mobase.FileTreeEntry] = [] + + for entry in filetree: + if isinstance(entry, mobase.IFileTree): + folders.append(entry) + else: + files.append(entry) + + VALID_FOLDERS = [ + "Cursors", + "DLC", + "Engine", + "Fonts", + "Localization", + "PakInfo", + "PlayerProfiles", + "Public", + "Shaders", + DivinityOriginalSinEnhancedEditionGame.DOCS_MOD_SPECIAL_NAME, + ] + for src_folder in folders: + for dst_folder in VALID_FOLDERS: + if src_folder.name().lower() == dst_folder.lower(): + return mobase.ModDataChecker.VALID + + VALID_FILE_EXTENSIONS = [ + ".pak", + ] + for src_file in files: + for extension in VALID_FILE_EXTENSIONS: + if src_file.name().lower().endswith(extension.lower()): + return mobase.ModDataChecker.VALID + + return mobase.ModDataChecker.INVALID + + +class DivinityOriginalSinEnhancedEditionGame(BasicGame, mobase.IPluginFileMapper): + Name = "Divinity: Original Sin (Enhanced Edition) Support Plugin" + Author = "LostDragonist" + Version = "1.0.0" + + GameName = "Divinity: Original Sin (Enhanced Edition)" + GameShortName = "divinityoriginalsinenhancededition" + GameNexusName = "divinityoriginalsinenhancededition" + GameValidShortNames = ["divinityoriginalsin"] + GameNexusId = 1995 + GameSteamId = [373420] + GameGogId = [1445516929, 1445524575] + GameBinary = "Shipping/EoCApp.exe" + GameDataPath = "Data" + GameSaveExtension = "lsv" + GameDocumentsDirectory = ( + "%USERPROFILE%/Documents/Larian Studios/Divinity Original Sin Enhanced Edition" + ) + GameSavesDirectory = ( + "%USERPROFILE%/Documents/Larian Studios/" + "Divinity Original Sin Enhanced Edition/PlayerProfiles" + ) + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Divinity:-Original-Sin" + ) + + DOCS_MOD_SPECIAL_NAME = "DOCS_MOD" + + def __init__(self): + BasicGame.__init__(self) + mobase.IPluginFileMapper.__init__(self) + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._register_feature(BasicGameSaveGameInfo(lambda s: s.with_suffix(".png"))) + self._register_feature(DivinityOriginalSinEnhancedEditionModDataChecker()) + return True + + def mappings(self) -> list[mobase.Mapping]: + map: list[mobase.Mapping] = [] + modDirs = self._listDirsRecursive(Path(self.DOCS_MOD_SPECIAL_NAME)) + for dir_ in modDirs: + for file_ in self._organizer.findFiles(path=dir_, filter=lambda x: True): + m = mobase.Mapping() + m.createTarget = True + m.isDirectory = False + m.source = file_ + m.destination = os.path.join( + self.documentsDirectory().absoluteFilePath("Mods"), + file_.split(self.DOCS_MOD_SPECIAL_NAME)[1].strip("\\").strip("/"), + ) + map.append(m) + return map + + def primarySources(self): + return self.GameValidShortNames + + def _listDirsRecursive(self, prefix: Path) -> list[str]: + res = [str(prefix)] + dirs = self._organizer.listDirectories(str(prefix)) + for dir_ in dirs: + res.extend(self._listDirsRecursive(prefix.joinpath(dir_))) + return res diff --git a/libs/basic_games/games/game_dragonsdogmadarkarisen.py b/libs/basic_games/games/game_dragonsdogmadarkarisen.py new file mode 100644 index 0000000..e26b00e --- /dev/null +++ b/libs/basic_games/games/game_dragonsdogmadarkarisen.py @@ -0,0 +1,19 @@ +from ..basic_game import BasicGame + + +class NoMansSkyGame(BasicGame): + Name = "Dragon's Dogma: Dark Arisen Support Plugin" + Author = "Luca/EzioTheDeadPoet" + Version = "1.0.0" + + GameName = "Dragon's Dogma: Dark Arisen" + GameShortName = "dragonsdogma" + GaneNexusHame = "dragonsdogma" + GameSteamId = 367500 + GameGogId = 1242384383 + GameBinary = "DDDA.exe" + GameDataPath = "nativePC" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Dragon's-Dogma:-Dark-Arisen" + ) diff --git a/libs/basic_games/games/game_dungeonsiege1.py b/libs/basic_games/games/game_dungeonsiege1.py new file mode 100644 index 0000000..4eb983d --- /dev/null +++ b/libs/basic_games/games/game_dungeonsiege1.py @@ -0,0 +1,92 @@ +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class DungeonSiegeIModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + + def get_resources_and_maps( + self, tree: mobase.IFileTree + ) -> tuple[list[mobase.FileTreeEntry], list[mobase.FileTreeEntry]]: + ress: list[mobase.FileTreeEntry] = [] + maps: list[mobase.FileTreeEntry] = [] + + for e in tree: + if e.isFile(): + if e.suffix().lower() == "dsres": + ress.append(e) + elif e.suffix().lower() == "dsmap": + maps.append(e) + + return ress, maps + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + # Check if we have a Resources / Maps folder or .dsres/.dsmap + ress, maps = self.get_resources_and_maps(filetree) + + if not ress and not maps: + if filetree.exists("Resources") or filetree.exists("Maps"): + return mobase.ModDataChecker.VALID + else: + return mobase.ModDataChecker.INVALID + + return mobase.ModDataChecker.FIXABLE + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + ress, maps = self.get_resources_and_maps(filetree) + + if ress: + rfolder = filetree.addDirectory("Resources") + for r in ress: + rfolder.insert(r, mobase.IFileTree.REPLACE) + if maps: + rfolder = filetree.addDirectory("Maps") + for r in maps: + rfolder.insert(r, mobase.IFileTree.REPLACE) + return filetree + + +class DungeonSiegeIGame(BasicGame): + Name = "Dungeon Siege I" + Author = "mrudat" + Version = "0.0.1" + + GameName = "Dungeon Siege I" + GameShortName = "dungeonsiege1" + GameNexusName = "dungeonsiege1" + GameNexusId = 541 + GameSteamId = [39190] + GameGogId = [1142020247] + GameBinary = "DungeonSiege.exe" + GameDataPath = "" + GameSavesDirectory = "%GAME_DOCUMENTS%/Save" + GameDocumentsDirectory = "%DOCUMENTS%/Dungeon Siege" + GameSaveExtension = "dssave" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Dungeon-Siege-I" + ) + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._register_feature(DungeonSiegeIModDataChecker()) + return True + + def executables(self): + execs = super().executables() + execs.append( + mobase.ExecutableInfo( + "Dungeon Siege Video Configuration", + QFileInfo(self.gameDirectory().absoluteFilePath("DSVideoConfig.exe")), + ) + ) + return execs + + def iniFiles(self): + return ["DungeonSiege.ini"] diff --git a/libs/basic_games/games/game_dungeonsiege2.py b/libs/basic_games/games/game_dungeonsiege2.py new file mode 100644 index 0000000..4a8f1de --- /dev/null +++ b/libs/basic_games/games/game_dungeonsiege2.py @@ -0,0 +1,92 @@ +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class DungeonSiegeIIModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + + def get_resources_and_maps( + self, tree: mobase.IFileTree + ) -> tuple[list[mobase.FileTreeEntry], list[mobase.FileTreeEntry]]: + ress: list[mobase.FileTreeEntry] = [] + maps: list[mobase.FileTreeEntry] = [] + + for e in tree: + if e.isFile(): + if e.suffix().lower() == "ds2res": + ress.append(e) + elif e.suffix().lower() == "ds2map": + maps.append(e) + + return ress, maps + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + # Check if we have a Resources / Maps folder or .ds2res/.ds2map + ress, maps = self.get_resources_and_maps(filetree) + + if not ress and not maps: + if filetree.exists("Resources") or filetree.exists("Maps"): + return mobase.ModDataChecker.VALID + else: + return mobase.ModDataChecker.INVALID + + return mobase.ModDataChecker.FIXABLE + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + ress, maps = self.get_resources_and_maps(filetree) + + if ress: + rfolder = filetree.addDirectory("Resources") + for r in ress: + rfolder.insert(r, mobase.IFileTree.REPLACE) + if maps: + rfolder = filetree.addDirectory("Maps") + for r in maps: + rfolder.insert(r, mobase.IFileTree.REPLACE) + return filetree + + +class DungeonSiegeIIGame(BasicGame): + Name = "Dungeon Siege II" + Author = "Holt59" + Version = "0.1.1" + + GameName = "Dungeon Siege II" + GameShortName = "dungeonsiegeii" + GameNexusName = "dungeonsiegeii" + GameNexusId = 2078 + GameSteamId = [39200] + GameGogId = [1142020247] + GameBinary = "DungeonSiege2.exe" + GameDataPath = "" + GameSavesDirectory = "%GAME_DOCUMENTS%/Save" + GameDocumentsDirectory = "%DOCUMENTS%/My Games/Dungeon Siege 2" + GameSaveExtension = "ds2party" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Dungeon-Siege-II" + ) + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._register_feature(DungeonSiegeIIModDataChecker()) + return True + + def executables(self): + execs = super().executables() + execs.append( + mobase.ExecutableInfo( + "Dungeon Siege Video Configuration", + QFileInfo(self.gameDirectory().absoluteFilePath("DS2VideoConfig.exe")), + ) + ) + return execs + + def iniFiles(self): + return ["DungeonSiege2.ini"] diff --git a/libs/basic_games/games/game_f123.py b/libs/basic_games/games/game_f123.py new file mode 100644 index 0000000..e740a40 --- /dev/null +++ b/libs/basic_games/games/game_f123.py @@ -0,0 +1,13 @@ +from ..basic_game import BasicGame + + +class F123Game(BasicGame): + Name = "F1 23 Support Plugin" + Author = "ju5tA1ex" + Version = "1.0.0" + + GameName = "F1 23" + GameShortName = "F1 23" + GameSteamId = 2108330 + GameBinary = "F1_23.exe" + GameDataPath = "" diff --git a/libs/basic_games/games/game_fantasylifei.py b/libs/basic_games/games/game_fantasylifei.py new file mode 100644 index 0000000..ae3cab1 --- /dev/null +++ b/libs/basic_games/games/game_fantasylifei.py @@ -0,0 +1,102 @@ +from pathlib import Path + +from PyQt6.QtCore import QDir + +import mobase + +from ..basic_features import BasicLocalSavegames, BasicModDataChecker, GlobPatterns +from ..basic_features.basic_save_game_info import BasicGameSaveGame +from ..basic_game import BasicGame +from ..steam_utils import find_steam_path + + +class FantasyLifeIModDataChecker(BasicModDataChecker): + def __init__(self): + super().__init__( + GlobPatterns( + move={ + "*.fliarchive": "Mods/", + "*.pak": "Paks/~mods/", + "*.ucas": "Paks/~mods/", + "*.utoc": "Paks/~mods/", + }, + valid=["L10N", "sound", "Mods", "Paks"], + ) + ) + + +class FantasyLifeI(BasicGame, mobase.IPluginFileMapper): + Name = "Fantasy Life I Support Plugin" + Author = "AmeliaCute" + Version = "0.2.2" + + GameName = "FANTASY LIFE i" + GameShortName = "fantasylifei" + GameNexusName = "fantasylifeithegirlwhostealstime" + GameValidShortNames = ["fli"] + + GameDataPath = "Game/Content/" + GameBinary = "Game/Binaries/Win64/NFL1-Win64-Shipping.exe" + GameSteamId = 2993780 + + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Fantasy-Life-I:-The-Girl-Who-Steals-Time" + ) + + def __init__(self): + BasicGame.__init__(self) + mobase.IPluginFileMapper.__init__(self) + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(FantasyLifeIModDataChecker()) + self._register_feature(BasicLocalSavegames(self)) + return True + + def executables(self): + return [ + mobase.ExecutableInfo("Fantasy Life I", self.GameBinary), + ] + + ## SAVE + # credit to game.darkestdungeon.py + @staticmethod + def getCloudSaveDirectory() -> str | None: + steamPath = find_steam_path() + if steamPath is None: + return None + + userData = steamPath.joinpath("userdata") + for child in userData.iterdir(): + name = child.name + try: + int(name) + except ValueError: + continue + + cloudSaves = child.joinpath("2993780/remote") + if cloudSaves.exists() and cloudSaves.is_dir(): + return str(cloudSaves) + return None + + def savesDirectory(self) -> QDir: + return QDir(self.getCloudSaveDirectory()) + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + saves: list[Path] = [] + for path in Path(folder.absolutePath()).glob("*.bin"): + saves.append(path) + + ##TODO: need a proper implementation + return [BasicGameSaveGame(path) for path in saves] + + ## MAPPING + + def exeDirectory(self) -> QDir: + return QDir(QDir(self.gameDirectory()).filePath("Game/Binaries/Win64")) + + def mappings(self) -> list[mobase.Mapping]: + return [ + mobase.Mapping("*.dll", self.exeDirectory().absolutePath(), False, True), + ] diff --git a/libs/basic_games/games/game_finalfantasy7rebirth.py b/libs/basic_games/games/game_finalfantasy7rebirth.py new file mode 100644 index 0000000..c592824 --- /dev/null +++ b/libs/basic_games/games/game_finalfantasy7rebirth.py @@ -0,0 +1,157 @@ +import math +import random +from pathlib import Path +from typing import Iterable, List + +import mobase + +from ..basic_features.basic_mod_data_checker import BasicModDataChecker, GlobPatterns +from ..basic_game import BasicGame + + +class FinalFantasy7RebirthDataChecker(BasicModDataChecker): + def __init__(self): + super().__init__( + GlobPatterns( + move={ + "*.pak": "End/Content/Paks/~Mods/", + "*.ucas": "End/Content/Paks/~Mods/", + "*.utoc": "End/Content/Paks/~Mods/", + "*.dll": "End/Binaries/Win64/", + }, + valid=[ + "End/Content/Paks/~Mods", + "End/Binaries/Win64", + "End", + "/", + "_ROOT", + "_ROOT/End/Binaries/Win64", + "_ROOT/End/Content/Paks/~Mods", + ], + ) + ) + + +class FinalFantasy7RebirthFileManager: + _mods_extensions = [".pak", ".ucas", ".utoc"] + _file_extensions = [".dll"] + + def __init__( + self, + mod_path: Path, + file_path: Path, + organizer: mobase.IOrganizer, + load_enabled: mobase.MoVariant, + ) -> None: + self._mod_path = mod_path + self._file_path = file_path + self._organizer = organizer + self._load_enabled = load_enabled + + def _active_mods(self) -> Iterable[Path]: + mods_path = Path(self._organizer.modsPath()) + mod_list = self._organizer.modList() + mod_load_order = mod_list.allModsByProfilePriority() + for mod in mod_load_order: + if mod_list.state(mod) & mobase.ModState.ACTIVE: + yield mods_path / mod + + def _recursive(self, child: Path, mod_prefix: str) -> Iterable[mobase.Mapping]: + if child.suffix.lower() in self._file_extensions: + dest_mod_path = self._file_path / child.name + yield mobase.Mapping(str(child), str(dest_mod_path), child.is_dir(), True) + + if child.suffix.lower() in self._mods_extensions: + child_name = ( + child.with_stem(mod_prefix + child.stem).name + if self._load_enabled + else child.name + ) + dest_mod_path = self._mod_path / child_name + yield mobase.Mapping(str(child), str(dest_mod_path), child.is_dir(), True) + + if child.is_dir(): + for inner in child.iterdir(): + yield from self._recursive(inner, mod_prefix) + + def mod_mapping(self) -> Iterable[mobase.Mapping]: + priority_digits = math.floor(math.log10(random.randrange(1, 999))) + 1 + for priority, mod_path in enumerate(self._active_mods()): + mod_prefix = str(priority).zfill(priority_digits) + "_" + for child in mod_path.iterdir(): + yield from self._recursive(child, mod_prefix) + + +class FinalFantasy7RebirthGame(BasicGame, mobase.IPluginFileMapper): + Name = "Final Fasntasy 7 Rebirth Support Plugin" + Author = "diegofesanto, TheUnlocked" + Version = "0.0.1" + + GameName = "Final Fantasy VII Rebirth" + GameShortName = "finalfantasy7rebirth" + GameNexuName = "finalfantasy7rebirth" + GameSteamId = 2909400 + GameBinary = "End/Binaries/Win64/ff7rebirth_.exe" + GameDataPath = "_ROOT" + GameSaveExtension = "sav" + + _forced_libraries = ["xinput1_3.dll"] + + def __init__(self): + BasicGame.__init__(self) + mobase.IPluginFileMapper.__init__(self) + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + + self._register_feature(FinalFantasy7RebirthDataChecker()) + + return True + + def settings(self) -> list[mobase.PluginSetting]: + return [ + mobase.PluginSetting( + "enforce_mod_load_order", + ("Force mod load order by list priority"), + True, + ) + ] + + def executables(self) -> list[mobase.ExecutableInfo]: + game_name = self.gameName() + game_dir = self.gameDirectory() + bin_path = game_dir.absoluteFilePath(self.binaryName()) + return [ + # Default + mobase.ExecutableInfo( + f"{game_name}", + bin_path, + ) + ] + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + exe = Path(self.binaryName()).name + return [ + mobase.ExecutableForcedLoadSetting(exe, lib).withEnabled(True) + for lib in self._forced_libraries + ] + + def _get_settings(self, key: str) -> mobase.MoVariant: + return self._organizer.pluginSetting(self.name(), key) + + def mappings(self) -> List[mobase.Mapping]: + mod_path = Path(self.gameDirectory().absolutePath()) / "End/Content/Paks/~Mods" + file_path = Path(self.gameDirectory().absolutePath()) / "End/Binaries/Win64" + load_enabled = self._get_settings("enforce_mod_load_order") + file_manager = FinalFantasy7RebirthFileManager( + mod_path, file_path, self._organizer, load_enabled + ) + + return [ + mobase.Mapping( + self._organizer.overwritePath(), + str(mod_path), + True, + True, + ) + ] + list(file_manager.mod_mapping()) diff --git a/libs/basic_games/games/game_finalfantasy7remake.py b/libs/basic_games/games/game_finalfantasy7remake.py new file mode 100644 index 0000000..44dd74f --- /dev/null +++ b/libs/basic_games/games/game_finalfantasy7remake.py @@ -0,0 +1,78 @@ +import math +from pathlib import Path +from typing import Iterable, List + +import mobase + +from ..basic_game import BasicGame + + +class FinalFantasy7RemakeGame(BasicGame, mobase.IPluginFileMapper): + Name = "Final Fantasy VII Remake Support Plugin" + Author = "TheUnlocked" + Version = "1.0.0" + + GameName = "Final Fantasy VII Remake" + GameShortName = "finalfantasy7remake" + GameNexusName = "finalfantasy7remake" + GameSteamId = 1462040 + GameBinary = "ff7remake.exe" + GameSaveExtension = "sav" + # _ROOT is a placeholder value. + # In order to properly apply load order to mods, custom mapping is used below. + GameDataPath = "_ROOT" + + def __init__(self): + BasicGame.__init__(self) + mobase.IPluginFileMapper.__init__(self) + + def init(self, organizer: mobase.IOrganizer): + return BasicGame.init(self, organizer) + + def _get_mods_path(self): + """The directory path of where .pak files from mods should go""" + return ( + Path(self.gameDirectory().absolutePath()) + / "End" + / "Content" + / "Paks" + / "~mods" + ) + + def _active_mod_paths(self) -> Iterable[Path]: + mods_parent_path = Path(self._organizer.modsPath()) + modlist = self._organizer.modList().allModsByProfilePriority() + for mod in modlist: + if self._organizer.modList().state(mod) & mobase.ModState.ACTIVE: + yield mods_parent_path / mod + + def _active_mod_mappings(self, mod_paths: List[Path]) -> Iterable[mobase.Mapping]: + if not mod_paths: + return + pak_priority_digits = math.floor(math.log10(len(mod_paths))) + 1 + + for priority, mod_path in enumerate(mod_paths): + pak_prefix = str(priority).zfill(pak_priority_digits) + "_" + for child in mod_path.iterdir(): + dest_path = ( + self._get_mods_path() + / child.with_stem(pak_prefix + child.stem).name + ) + if child.is_dir() or child.suffix.lower() == ".pak": + yield mobase.Mapping( + str(child), + str(dest_path), + child.is_dir(), + ) + + def mappings(self) -> List[mobase.Mapping]: + return [ + # Not applying load order modifications to overwrites is OK + # since the UX is better this way and it will generally work out anyways + mobase.Mapping( + self._organizer.overwritePath(), + str(self._get_mods_path()), + True, + True, + ) + ] + list(self._active_mod_mappings(list(self._active_mod_paths()))) diff --git a/libs/basic_games/games/game_gta-3-de.py b/libs/basic_games/games/game_gta-3-de.py new file mode 100644 index 0000000..c9fc338 --- /dev/null +++ b/libs/basic_games/games/game_gta-3-de.py @@ -0,0 +1,72 @@ +import os +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class GTA3DefinitiveEditionModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + for entry in filetree: + if Path(entry.name().casefold()).suffix == ".pak": + return mobase.ModDataChecker.VALID + + return mobase.ModDataChecker.INVALID + + +class GTA3DefinitiveEditionGame(BasicGame): + Name = "Grand Theft Auto III - Definitive Edition Support Plugin" + Author = "dekart811" + Version = "1.0" + + GameName = "GTA III - Definitive Edition" + GameShortName = "grandtheftautothetrilogy" + GameNexusName = "grandtheftautothetrilogy" + GameBinary = "Gameface/Binaries/Win64/LibertyCity.exe" + GameDataPath = "Gameface/Content/Paks/~mods" + GameDocumentsDirectory = ( + "%USERPROFILE%/Documents/Rockstar Games/" + "GTA III Definitive Edition/Config/WindowsNoEditor" + ) + GameSavesDirectory = "%GAME_DOCUMENTS%/../../SaveGames" + GameSaveExtension = "sav" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Grand-Theft-Auto:-The-Trilogy-%E2%80%90-The-Definitive-Edition" + ) + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(GTA3DefinitiveEditionModDataChecker()) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "GTA III - Definitive Edition", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "GTA III - Definitive Edition (DirectX 12)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("-dx12"), + ] + + def iniFiles(self): + return ["GameUserSettings.ini", "CustomSettings.ini"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + # Create the mods directory if it doesn't exist + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + + super().initializeProfile(directory, settings) diff --git a/libs/basic_games/games/game_gta-san-andreas-de.py b/libs/basic_games/games/game_gta-san-andreas-de.py new file mode 100644 index 0000000..200a562 --- /dev/null +++ b/libs/basic_games/games/game_gta-san-andreas-de.py @@ -0,0 +1,72 @@ +import os +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class GTASanAndreasDefinitiveEditionModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + for entry in filetree: + if Path(entry.name().casefold()).suffix == ".pak": + return mobase.ModDataChecker.VALID + + return mobase.ModDataChecker.INVALID + + +class GTASanAndreasDefinitiveEditionGame(BasicGame): + Name = "Grand Theft Auto: San Andreas - Definitive Edition Support Plugin" + Author = "dekart811" + Version = "1.0" + + GameName = "GTA: San Andreas - Definitive Edition" + GameShortName = "grandtheftautothetrilogy" + GameNexusName = "grandtheftautothetrilogy" + GameBinary = "Gameface/Binaries/Win64/SanAndreas.exe" + GameDataPath = "Gameface/Content/Paks/~mods" + GameDocumentsDirectory = ( + "%USERPROFILE%/Documents/Rockstar Games/" + "GTA San Andreas Definitive Edition/Config/WindowsNoEditor" + ) + GameSavesDirectory = "%GAME_DOCUMENTS%/../../SaveGames" + GameSaveExtension = "sav" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Grand-Theft-Auto:-The-Trilogy-%E2%80%90-The-Definitive-Edition" + ) + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(GTASanAndreasDefinitiveEditionModDataChecker()) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "GTA: San Andreas - Definitive Edition", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "GTA: San Andreas - Definitive Edition (DirectX 12)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("-dx12"), + ] + + def iniFiles(self): + return ["GameUserSettings.ini", "CustomSettings.ini"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + # Create the mods directory if it doesn't exist + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + + super().initializeProfile(directory, settings) diff --git a/libs/basic_games/games/game_gta-vice-city-de.py b/libs/basic_games/games/game_gta-vice-city-de.py new file mode 100644 index 0000000..fa068e6 --- /dev/null +++ b/libs/basic_games/games/game_gta-vice-city-de.py @@ -0,0 +1,72 @@ +import os +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class GTAViceCitysDefinitiveEditionModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + for entry in filetree: + if Path(entry.name().casefold()).suffix == ".pak": + return mobase.ModDataChecker.VALID + + return mobase.ModDataChecker.INVALID + + +class GTAViceCityDefinitiveEditionGame(BasicGame): + Name = "Grand Theft Auto: Vice City - Definitive Edition Support Plugin" + Author = "dekart811" + Version = "1.0" + + GameName = "GTA: Vice City - Definitive Edition" + GameShortName = "grandtheftautothetrilogy" + GameNexusName = "grandtheftautothetrilogy" + GameBinary = "Gameface/Binaries/Win64/ViceCity.exe" + GameDataPath = "Gameface/Content/Paks/~mods" + GameDocumentsDirectory = ( + "%USERPROFILE%/Documents/Rockstar Games/" + "GTA Vice City Definitive Edition/Config/WindowsNoEditor" + ) + GameSavesDirectory = "%GAME_DOCUMENTS%/../../SaveGames" + GameSaveExtension = "sav" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Grand-Theft-Auto:-The-Trilogy-%E2%80%90-The-Definitive-Edition" + ) + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(GTAViceCitysDefinitiveEditionModDataChecker()) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "GTA: Vice City - Definitive Edition", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "GTA: Vice City - Definitive Edition (DirectX 12)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("-dx12"), + ] + + def iniFiles(self): + return ["GameUserSettings.ini", "CustomSettings.ini"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + # Create the mods directory if it doesn't exist + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + + super().initializeProfile(directory, settings) diff --git a/libs/basic_games/games/game_kerbalspaceprogram.py b/libs/basic_games/games/game_kerbalspaceprogram.py new file mode 100644 index 0000000..fec834f --- /dev/null +++ b/libs/basic_games/games/game_kerbalspaceprogram.py @@ -0,0 +1,61 @@ +from pathlib import Path + +from PyQt6.QtCore import QDir + +import mobase + +from ..basic_features import BasicGameSaveGameInfo +from ..basic_features.basic_save_game_info import BasicGameSaveGame +from ..basic_game import BasicGame + + +class KerbalSpaceProgramSaveGame(BasicGameSaveGame): + def allFiles(self): + files = [super().getFilepath()] + banner = self._filepath.joinpath("banners").joinpath(f"${self.getName()}.png") + if banner.exists(): + files.append(banner.as_posix()) + return files + + def getName(self): + return self._filepath.stem + + def getSaveGroupIdentifier(self): + return self._filepath.parent.name + + +class KerbalSpaceProgramGame(BasicGame): + Name = "Kerbal Space Program Support Plugin" + Author = "LaughingHyena" + Version = "1.0.0" + + GameName = "Kerbal Space Program" + GameShortName = "kerbalspaceprogram" + GameNexusName = "kerbalspaceprogram" + GameSteamId = [220200, 283740, 982970] + GameBinary = "KSP_x64.exe" + GameDataPath = "GameData" + GameSavesDirectory = "%GAME_PATH%/saves" + GameSaveExtension = "sfs" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Kerbal-Space-Program" + ) + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._register_feature( + BasicGameSaveGameInfo( + lambda s: str( + Path(s).parent.joinpath("banners").joinpath(f"{Path(s).stem}.png") + ) + ) + ) + return True + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + KerbalSpaceProgramSaveGame(path) + for path in Path(folder.absolutePath()).glob(f"*/*.{ext}") + ] diff --git a/libs/basic_games/games/game_kingdomcomedeliverance.py b/libs/basic_games/games/game_kingdomcomedeliverance.py new file mode 100644 index 0000000..0bc5fe1 --- /dev/null +++ b/libs/basic_games/games/game_kingdomcomedeliverance.py @@ -0,0 +1,48 @@ +import os + +from PyQt6.QtCore import QDir + +import mobase + +from ..basic_game import BasicGame + + +class KingdomComeDeliveranceGame(BasicGame): + Name = "Kingdom Come Deliverance Support Plugin" + Author = "Silencer711" + Version = "1.0.0" + + GameName = "Kingdom Come: Deliverance" + GameShortName = "kingdomcomedeliverance" + GameNexusName = "kingdomcomedeliverance" + GameNexusId = 2298 + GameSteamId = [379430] + GameGogId = [1719198803] + GameEpicId = "Eel" + GameBinary = "bin/Win64/KingdomCome.exe" + GameDataPath = "mods" + GameSaveExtension = "whs" + GameDocumentsDirectory = "%GAME_PATH%" + GameSavesDirectory = "%USERPROFILE%/Saved Games/kingdomcome/saves" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Kingdom-Come:-Deliverance" + ) + + def iniFiles(self): + return ["custom.cfg", "system.cfg", "user.cfg"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + # Create .cfg files if they don't exist + for iniFile in self.iniFiles(): + iniPath = self.documentsDirectory().absoluteFilePath(iniFile) + if not os.path.exists(iniPath): + with open(iniPath, "w") as _: + pass + + # Create the mods directory if it doesn't exist + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + + super().initializeProfile(directory, settings) diff --git a/libs/basic_games/games/game_masterduel.py b/libs/basic_games/games/game_masterduel.py new file mode 100644 index 0000000..8c23512 --- /dev/null +++ b/libs/basic_games/games/game_masterduel.py @@ -0,0 +1,89 @@ +from pathlib import Path +from typing import List, Optional + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class MasterDuelGame(BasicGame, mobase.IPluginFileMapper): + Name = "Yu-Gi-Oh! Master Duel Support Plugin" + Author = "The Conceptionist & uwx" + Version = "1.0.2" + Description = ( + "Adds support for basic Yu-Gi-Oh! Master Duel mods.\n" + 'Eligible folders for mods are "0000" and "AssetBundle".' + ) + + GameName = "Yu-Gi-Oh! Master Duel" + GameShortName = "masterduel" + GameNexusName = "yugiohmasterduel" + GameNexusId = 4272 + GameSteamId = 1449850 + GameBinary = "masterduel.exe" + + def __init__(self): + BasicGame.__init__(self) + mobase.IPluginFileMapper.__init__(self) + + def executables(self): + return [ + mobase.ExecutableInfo( + "Yu-Gi-Oh! Master Duel", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("-popupwindow"), + ] + + # dataDirectory returns the specific LocalData folder because + # this is where most mods will go to + def dataDirectory(self) -> QDir: + return QDir(self.userDataDir()) + + _userDataDirCached: Optional[str] = None + + # Gets the LocalData/xxxxxxxxx directory. This directory has a different, + # unique, 8-character hex name for each user. + def userDataDir(self) -> str: + if self._userDataDirCached is not None: + return self._userDataDirCached + + dir = self.gameDirectory() + dir.cd("LocalData") + + subdirs = dir.entryList(filters=QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot) + dir.cd(subdirs[0]) + + self._userDataDirCached = dir.absolutePath() + return self._userDataDirCached + + def mappings(self) -> List[mobase.Mapping]: + modsPath = Path(self._organizer.modsPath()) + unityMods = self.getUnityDataMods() + + mappings: List[mobase.Mapping] = [] + + for modName in unityMods: + m = mobase.Mapping() + m.createTarget = False + m.isDirectory = True + m.source = modsPath.joinpath(modName, "AssetBundle").as_posix() + m.destination = self.gameDirectory().filePath( + Path("masterduel_Data", "StreamingAssets", "AssetBundle").as_posix() + ) + mappings.append(m) + + return mappings + + def getUnityDataMods(self) -> list[str]: + modsPath = Path(self._organizer.modsPath()) + allMods = self._organizer.modList().allModsByProfilePriority() + + unityMods: list[str] = [] + for modName in allMods: + if self._organizer.modList().state(modName) & mobase.ModState.ACTIVE != 0: + if modsPath.joinpath(modName, "AssetBundle").exists(): + unityMods.append(modName) + + return unityMods diff --git a/libs/basic_games/games/game_metalgearsolid2mc.py b/libs/basic_games/games/game_metalgearsolid2mc.py new file mode 100644 index 0000000..30804b8 --- /dev/null +++ b/libs/basic_games/games/game_metalgearsolid2mc.py @@ -0,0 +1,17 @@ +from ..basic_game import BasicGame + + +class MetalGearSolid2MCGame(BasicGame): + Name = ( + "METAL GEAR SOLID 2: Sons of Liberty - Master Collection Version Support Plugin" + ) + Author = "AkiraJkr" + Version = "1.0.0" + + GameName = "METAL GEAR SOLID 2: Sons of Liberty - Master Collection Version" + GameShortName = "metalgearsolid2mc" + GameNexusName = "metalgearsolid2mc" + GameSteamId = 2131640 + GameBinary = "METAL GEAR SOLID2.exe" + GameDataPath = "" + GameLauncher = "launcher.exe" diff --git a/libs/basic_games/games/game_metalgearsolid3mc.py b/libs/basic_games/games/game_metalgearsolid3mc.py new file mode 100644 index 0000000..10f669c --- /dev/null +++ b/libs/basic_games/games/game_metalgearsolid3mc.py @@ -0,0 +1,15 @@ +from ..basic_game import BasicGame + + +class MetalGearSolid3MCGame(BasicGame): + Name = "METAL GEAR SOLID 3: Snake Eater - Master Collection Version Support Plugin" + Author = "AkiraJkr" + Version = "1.0.0" + + GameName = "METAL GEAR SOLID 3: Snake Eater - Master Collection Version" + GameShortName = "metalgearsolid3mc" + GameNexusName = "metalgearsolid3mc" + GameSteamId = 2131650 + GameBinary = "METAL GEAR SOLID3.exe" + GameDataPath = "" + GameLauncher = "launcher.exe" diff --git a/libs/basic_games/games/game_mirrorsedge.py b/libs/basic_games/games/game_mirrorsedge.py new file mode 100644 index 0000000..db81908 --- /dev/null +++ b/libs/basic_games/games/game_mirrorsedge.py @@ -0,0 +1,19 @@ +from ..basic_game import BasicGame + + +class MirrorsEdgeGame(BasicGame): + Name = "Mirror's Edge Support Plugin" + Author = "Luca/EzioTheDeadPoet" + Version = "1.0.0" + + GameName = "Mirror's Edge" + GameShortName = "mirrorsedge" + GaneNexusHame = "" + GameSteamId = 17410 + GameGogId = 1893001152 + GameBinary = "Binaries/MirrorsEdge.exe" + GameDataPath = "TdGame" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Mirror's-Edge" + ) diff --git a/libs/basic_games/games/game_monsterhunterrise.py b/libs/basic_games/games/game_monsterhunterrise.py new file mode 100644 index 0000000..e63ed10 --- /dev/null +++ b/libs/basic_games/games/game_monsterhunterrise.py @@ -0,0 +1,15 @@ +from ..basic_game import BasicGame + + +class MonsterHunterRiseGame(BasicGame): + Name = "Monster Hunter: Rise Support Plugin" + Author = "RodolfoFigueroa" + Version = "1.0.0" + + GameName = "Monster Hunter: Rise" + GameShortName = "monsterhunterrise" + GameBinary = "MonsterHunterRise.exe" + GameDataPath = "%GAME_PATH%" + GameSaveExtension = "bin" + GameNexusId = 4095 + GameSteamId = 1446780 diff --git a/libs/basic_games/games/game_monsterhunterworld.py b/libs/basic_games/games/game_monsterhunterworld.py new file mode 100644 index 0000000..290048c --- /dev/null +++ b/libs/basic_games/games/game_monsterhunterworld.py @@ -0,0 +1,21 @@ +from ..basic_game import BasicGame + + +class MonsterHunterWorldGame(BasicGame): + Name = "Monster Hunter: World Support Plugin" + Author = "prz" + Version = "1.0.0" + + GameName = "Monster Hunter: World" + GameShortName = "monsterhunterworld" + GameNexusName = "monsterhunterworld" + GameNexusId = 2531 + GameBinary = "MonsterHunterWorld.exe" + GameLauncher = "MonsterHunterWorld.exe" + GameDataPath = "%GAME_PATH%" + GameSaveExtension = "dat" + GameSteamId = 582010 + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Monster-Hunter:-World" + ) diff --git a/libs/basic_games/games/game_mountandblade2.py b/libs/basic_games/games/game_mountandblade2.py new file mode 100644 index 0000000..df2c157 --- /dev/null +++ b/libs/basic_games/games/game_mountandblade2.py @@ -0,0 +1,100 @@ +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_features import BasicLocalSavegames +from ..basic_game import BasicGame, BasicGameSaveGame + + +class MountAndBladeIIModDataChecker(mobase.ModDataChecker): + _valid_folders: list[str] = [ + "native", + "sandbox", + "sandboxcore", + "storymode", + "custombattle", + ] + + def __init__(self): + super().__init__() + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + for e in filetree: + if e.isDir(): + if e.name().lower() in self._valid_folders: + return mobase.ModDataChecker.VALID + if e.exists("SubModule.xml", mobase.IFileTree.FILE): # type: ignore + return mobase.ModDataChecker.VALID + + return mobase.ModDataChecker.INVALID + + +class MountAndBladeIIGame(BasicGame): + Name = "Mount & Blade II: Bannerlord" + Author = "Holt59" + Version = "0.1.1" + Description = "Adds support for Mount & Blade II: Bannerlord" + + GameName = "Mount & Blade II: Bannerlord" + GameShortName = "mountandblade2bannerlord" + GameDataPath = "Modules" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Mount-&-Blade-II:-Bannerlord" + ) + + GameBinary = "bin/Win64_Shipping_Client/TaleWorlds.MountAndBlade.Launcher.exe" + + GameDocumentsDirectory = "%DOCUMENTS%/Mount and Blade II Bannerlord/Configs" + GameSavesDirectory = "%DOCUMENTS%/Mount and Blade II Bannerlord/Game Saves" + + GameNexusId = 3174 + GameSteamId = 261550 + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._register_feature(MountAndBladeIIModDataChecker()) + self._register_feature(BasicLocalSavegames(self)) + return True + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + save_paths = list(Path(folder.absolutePath()).glob("*.sav")) + list( + Path(folder.absolutePath()).glob("*.sav.cleaner_backup_*") + ) + return [BasicGameSaveGame(path) for path in save_paths] + + def executables(self): + return [ + mobase.ExecutableInfo( + "Mount & Blade II: Bannerlord (Launcher)", + QFileInfo( + self.gameDirectory(), + "bin/Win64_Shipping_Client/TaleWorlds.MountAndBlade.Launcher.exe", + ), + ), + mobase.ExecutableInfo( + "Mount & Blade II: Bannerlord", + QFileInfo( + self.gameDirectory(), + "bin/Win64_Shipping_Client/Bannerlord.exe", + ), + ), + mobase.ExecutableInfo( + "Mount & Blade II: Bannerlord (Native)", + QFileInfo( + self.gameDirectory(), + "bin/Win64_Shipping_Client/Bannerlord.Native.exe", + ), + ), + mobase.ExecutableInfo( + "Mount & Blade II: Bannerlord (BE)", + QFileInfo( + self.gameDirectory(), + "bin/Win64_Shipping_Client/Bannerlord_BE.exe", + ), + ), + ] diff --git a/libs/basic_games/games/game_msfs2020.py b/libs/basic_games/games/game_msfs2020.py new file mode 100644 index 0000000..d0c91ba --- /dev/null +++ b/libs/basic_games/games/game_msfs2020.py @@ -0,0 +1,34 @@ +import os +import re + +from PyQt6.QtCore import QDir + +from ..basic_game import BasicGame + + +class MSFS2020Game(BasicGame): + Name = "Microsoft Flight Simulator 2020 Support Plugin" + Author = "Deorder" + Version = "0.0.1" + + GameName = "Microsoft Flight Simulator 2020" + GameShortName = "msfs2020" + GameBinary = r"FlightSimulator.exe" + GameSteamId = [1250410] + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Microsoft-Flight-Simulator-(2020)" + ) + + def dataDirectory(self) -> QDir: + # Find and use package path specified in Asobo engine options + AppDataPath = os.path.expandvars(r"%APPDATA%\Microsoft Flight Simulator") + UserCfgPath = os.path.join(AppDataPath, "UserCfg.opt") + InstalledPackagesPathPattern = re.compile( + r'InstalledPackagesPath\s*=\s*"(.*)"', re.IGNORECASE + ) + with open(UserCfgPath, newline="") as f: + for _, line in enumerate(f): + for match in re.finditer(InstalledPackagesPathPattern, line): + return QDir(os.path.join(match.group(), "Community")) + return QDir(os.path.join(AppDataPath, "Packages", "Community")) diff --git a/libs/basic_games/games/game_nfshs.py b/libs/basic_games/games/game_nfshs.py new file mode 100644 index 0000000..ef24234 --- /dev/null +++ b/libs/basic_games/games/game_nfshs.py @@ -0,0 +1,28 @@ +# -*- encoding: utf-8 -*- + +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class NFSHSGame(BasicGame): + Name = "Need for Speed: High Stakes Support Plugin" + Author = "uwx" + Version = "1.0.0" + + GameName = "Need for Speed: High Stakes" + GameShortName = "nfshs" + GameNexusName = "needforspeedhighstakes" + GameNexusId = 6032 + GameBinary = "nfshs.exe" + GameDataPath = "" + + def executables(self): + return [ + mobase.ExecutableInfo( + "Need for Speed: High Stakes", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] diff --git a/libs/basic_games/games/game_nierautomata.py b/libs/basic_games/games/game_nierautomata.py new file mode 100644 index 0000000..3026fb1 --- /dev/null +++ b/libs/basic_games/games/game_nierautomata.py @@ -0,0 +1,14 @@ +from ..basic_game import BasicGame + + +class NierAutomataGame(BasicGame): + Name = "NieR:Automata Support Plugin" + Author = "Luca/EzioTheDeadPoet" + Version = "1.0.0" + + GameName = "NieR:Automata" + GameShortName = "nierautomata" + GameNexusName = "nierautomata" + GameSteamId = 524220 + GameBinary = "NieRAutomata.exe" + GameDataPath = "" diff --git a/libs/basic_games/games/game_nomanssky.py b/libs/basic_games/games/game_nomanssky.py new file mode 100644 index 0000000..651a326 --- /dev/null +++ b/libs/basic_games/games/game_nomanssky.py @@ -0,0 +1,38 @@ +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class NoMansSkyGame(BasicGame): + Name = "No Man's Sky Support Plugin" + Author = "Luca/EzioTheDeadPoet" + Version = "1.0.0" + + GameName = "No Man's Sky" + GameShortName = "nomanssky" + GameNexusName = "nomanssky" + GameNexusId = 1634 + GameSteamId = 275850 + GameGogId = 1446213994 + GameBinary = "Binaries/NMS.exe" + GameDataPath = "GAMEDATA/MODS" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-No-Man's-Sky" + ) + GameSavesDirectory = "%USERPROFILE%/AppData/Roaming/HelloGames/NMS" + GameSaveExtension = "hg" + + def executables(self): + return [ + mobase.ExecutableInfo( + "No Man's Sky", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "No Man's Sky VR", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("-HmdEnable 1"), + ] diff --git a/libs/basic_games/games/game_oblivion_remaster.py b/libs/basic_games/games/game_oblivion_remaster.py new file mode 100644 index 0000000..218bb04 --- /dev/null +++ b/libs/basic_games/games/game_oblivion_remaster.py @@ -0,0 +1,441 @@ +import json +import os.path +import shutil +import winreg +from enum import IntEnum, auto +from pathlib import Path +from typing import cast + +from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget + +import mobase + +from ..basic_features import BasicGameSaveGameInfo +from ..basic_game import BasicGame +from .oblivion_remaster.constants import DEFAULT_UE4SS_MODS, PLUGIN_NAME, UE4SSModInfo +from .oblivion_remaster.paks.widget import PaksTabWidget +from .oblivion_remaster.ue4ss.widget import UE4SSTabWidget + + +def getLootPath() -> Path | None: + """ + Parse the LOOT path using either the modern InnoSetup registry entries (local vs. global installs) or the + old registry path. + """ + paths = [ + ( + winreg.HKEY_CURRENT_USER, + "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BF634210-A0D4-443F-A657-0DCE38040374}_is1", + "InstallLocation", + ), + ( + winreg.HKEY_LOCAL_MACHINE, + "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BF634210-A0D4-443F-A657-0DCE38040374}_is1", + "InstallLocation", + ), + (winreg.HKEY_LOCAL_MACHINE, "Software\\LOOT", "Installed Path"), + ] + + for path in paths: + try: + with winreg.OpenKeyEx( + path[0], + path[1], + ) as key: + value = winreg.QueryValueEx(key, path[2]) + return Path((value[0] + "/LOOT.exe").replace("/", "\\")) + except FileNotFoundError: + pass + return None + + +class Problems(IntEnum): + """ + Enums for IPluginDiagnose. + """ + + # The 'dwmapi.dll' is present in the game EXE directory. + UE4SS_LOADER = auto() + # A UE4SS mod has a space in the directory name. + INVALID_UE4SS_MOD_NAME = auto() + + +class OblivionRemasteredGame( + BasicGame, mobase.IPluginFileMapper, mobase.IPluginDiagnose +): + Name = PLUGIN_NAME + Author = "Silarn" + Version = "0.1.0-b.1" + Description = "TES IV: Oblivion Remastered; an unholy hybrid of Bethesda and Unreal" + + GameName = "Oblivion Remastered" + GameShortName = "oblivionremastered" + GameNexusId = 7587 + GameSteamId = 2623190 + GameBinary = "OblivionRemastered.exe" + GameDataPath = r"%GAME_PATH%\OblivionRemastered\Content\Dev\ObvData\Data" + GameDocumentsDirectory = r"%GAME_PATH%\OblivionRemastered\Content\Dev\ObvData" + UserHome = QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.HomeLocation + ) + # Oblivion Remastered does not use the expanded Documents path but instead always uses the + # base user directory path, even when this disagrees with Windows. + MyDocumentsDirectory = rf"{UserHome}\Documents\My Games\{GameName}" + GameSavesDirectory = rf"{MyDocumentsDirectory}\Saved\SaveGames" + GameSaveExtension = "sav" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Elder-Scrolls-IV:-Oblivion-Remastered" + ) + + _main_window: QMainWindow + _ue4ss_tab: UE4SSTabWidget + _paks_tab: PaksTabWidget + + def __init__(self): + BasicGame.__init__(self) + mobase.IPluginFileMapper.__init__(self) + mobase.IPluginDiagnose.__init__(self) + + def init(self, organizer: mobase.IOrganizer) -> bool: + from .oblivion_remaster.game_plugins import OblivionRemasteredGamePlugins + from .oblivion_remaster.mod_data_checker import OblivionRemasteredModDataChecker + from .oblivion_remaster.mod_data_content import OblivionRemasteredDataContent + from .oblivion_remaster.script_extender import OblivionRemasteredScriptExtender + + super().init(organizer) + self._register_feature(BasicGameSaveGameInfo()) + self._register_feature(OblivionRemasteredGamePlugins(self._organizer)) + self._register_feature(OblivionRemasteredModDataChecker(self._organizer)) + self._register_feature(OblivionRemasteredScriptExtender(self)) + self._register_feature(OblivionRemasteredDataContent()) + + organizer.onUserInterfaceInitialized(self.init_tab) + return True + + def init_tab(self, main_window: QMainWindow): + """ + Initializes tabs unique to Oblivion Remastered. + The "UE4SS Mods" tab and "Paks" tab. + """ + if self._organizer.managedGame() != self: + return + + self._main_window = main_window + tab_widget: QTabWidget = main_window.findChild(QTabWidget, "tabWidget") + if not tab_widget or not tab_widget.findChild(QWidget, "espTab"): + return + + self._ue4ss_tab = UE4SSTabWidget(main_window, self._organizer) + + # Find the default "Plugins" tab + plugin_tab = tab_widget.findChild(QWidget, "espTab") + tab_index = tab_widget.indexOf(plugin_tab) + 1 + # The "Bethesda Plugins Manager" plugin hides the default Plugins tab and inserts itself after. + # If the default tab is hidden, increment our position by one to account for it. + if not tab_widget.isTabVisible(tab_widget.indexOf(plugin_tab)): + tab_index += 1 + tab_widget.insertTab(tab_index, self._ue4ss_tab, "UE4SS Mods") + + self._paks_tab = PaksTabWidget(main_window, self._organizer) + + tab_index += 1 + tab_widget.insertTab(tab_index, self._paks_tab, "Paks") + + def settings(self) -> list[mobase.PluginSetting]: + return [ + mobase.PluginSetting( + "ue4ss_use_root_builder", "Use Root Builder paths for UE4SS mods", False + ) + ] + + def executables(self): + execs = [ + mobase.ExecutableInfo( + "Oblivion Remastered", + QFileInfo( + QDir( + self.gameDirectory().absoluteFilePath( + "OblivionRemastered/Binaries/Win64" + ) + ), + "OblivionRemastered-Win64-Shipping.exe", + ), + ) + ] + if extender := self._organizer.gameFeatures().gameFeature( + mobase.ScriptExtender + ): + execs.append( + mobase.ExecutableInfo("OBSE64", QFileInfo(extender.loaderPath())) # type: ignore + ) + if lootPath := getLootPath(): + execs.append( + mobase.ExecutableInfo("LOOT", QFileInfo(str(lootPath))).withArgument( + '--game="Oblivion Remastered"' + ) + ) + if magicLoaderPath := self.gameDirectory().absoluteFilePath( + "MagicLoader/MagicLoader.exe" + ): + execs.append( + mobase.ExecutableInfo("MagicLoader", QFileInfo(magicLoaderPath)) + ) + + return execs + + def primaryPlugins(self) -> list[str]: + return [ + "Oblivion.esm", + "DLCBattlehornCastle.esp", + "DLCFrostcrag.esp", + "DLCHorseArmor.esp", + "DLCMehrunesRazor.esp", + "DLCOrrery.esp", + "DLCShiveringIsles.esp", + "DLCSpellTomes.esp", + "DLCThievesDen.esp", + "DLCVileLair.esp", + "Knights.esp", + "AltarESPMain.esp", + "AltarDeluxe.esp", + "AltarESPLocal.esp", # Not actually shipped with the game files but present in plugins.txt. + ] + + def modDataDirectory(self) -> str: + return "Data" + + def moviesDirectory(self) -> QDir: + return QDir( + self.gameDirectory().absolutePath() + "/OblivionRemastered/Content/Movies" + ) + + def paksDirectory(self) -> QDir: + return QDir( + self.gameDirectory().absolutePath() + "/OblivionRemastered/Content/Paks" + ) + + def exeDirectory(self) -> QDir: + return QDir( + self.gameDirectory().absolutePath() + "/OblivionRemastered/Binaries/Win64" + ) + + def obseDirectory(self) -> QDir: + return QDir(self.exeDirectory().absolutePath() + "/OBSE") + + def ue4ssDirectory(self) -> QDir: + return QDir(self.exeDirectory().absolutePath() + "/ue4ss/Mods") + + def loadOrderMechanism(self) -> mobase.LoadOrderMechanism: + return mobase.LoadOrderMechanism.PLUGINS_TXT + + def sortMechanism(self) -> mobase.SortMechanism: + return mobase.SortMechanism.LOOT + + def initializeProfile( + self, directory: QDir, settings: mobase.ProfileSetting + ) -> None: + if settings & mobase.ProfileSetting.CONFIGURATION: + game_ini_file = self.gameDirectory().absoluteFilePath( + r"OblivionRemastered\Content\Dev\ObvData\Oblivion.ini" + ) + game_default_ini = self.gameDirectory().absoluteFilePath( + r"OblivionRemastered\Content\Dev\ObvData\Oblivion_default.ini" + ) + profile_ini = directory.absoluteFilePath( + QFileInfo("Oblivion.ini").fileName() + ) + if not os.path.exists(profile_ini): + try: + shutil.copyfile( + game_ini_file, + profile_ini, + ) + except FileNotFoundError: + if os.path.exists(game_ini_file): + shutil.copyfile( + game_default_ini, + profile_ini, + ) + else: + Path(profile_ini).touch() + # Initialize a default UE4SS mods.ini and mods.json with the core mods included + self.write_default_mods(directory) + # Bootstrap common mod directories used by the USVFS map + if ( + self._organizer.managedGame() + and self._organizer.managedGame().gameName() == self.gameName() + ): + if not self.paksDirectory().exists(): + os.makedirs(self.paksDirectory().absolutePath()) + if not self.obseDirectory().exists(): + os.makedirs(self.obseDirectory().absolutePath()) + if not self.ue4ssDirectory().exists(): + os.makedirs(self.ue4ssDirectory().absolutePath()) + + def write_default_mods(self, profile: QDir): + """ + Writer for the default UE4SS 'mods.txt' and 'mods.json' profile files. + """ + + ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if not ue4ss_mods_txt.exists(): + with open(ue4ss_mods_txt.absoluteFilePath(), "w") as mods_txt: + for mod in DEFAULT_UE4SS_MODS: + mods_txt.write(f"{mod['mod_name']} : 1\n") + if not ue4ss_mods_json.exists(): + mods_data: list[UE4SSModInfo] = [] + for mod in DEFAULT_UE4SS_MODS: + mods_data.append({"mod_name": mod["mod_name"], "mod_enabled": True}) + with open(ue4ss_mods_json.absoluteFilePath(), "w") as mods_json: + mods_json.write(json.dumps(mods_data, indent=4)) + + def iniFiles(self) -> list[str]: + return ["Oblivion.ini"] + + def mappings(self) -> list[mobase.Mapping]: + mappings: list[mobase.Mapping] = [] + for profile_file in ["plugins.txt", "loadorder.txt"]: + mappings.append( + mobase.Mapping( + self._organizer.profilePath() + "/" + profile_file, + self.dataDirectory().absolutePath() + "/" + profile_file, + False, + ) + ) + for profile_file in ["mods.txt", "mods.json"]: + mappings.append( + mobase.Mapping( + self._organizer.profilePath() + "/" + profile_file, + self.ue4ssDirectory().absolutePath() + "/" + profile_file, + False, + ) + ) + return mappings + + def getModMappings(self) -> dict[str, list[str]]: + return { + "Data": [self.dataDirectory().absolutePath()], + "Paks": [self.paksDirectory().absolutePath()], + "OBSE": [self.obseDirectory().absolutePath()], + "Movies": [self.moviesDirectory().absolutePath()], + "UE4SS": [self.ue4ssDirectory().absolutePath()], + "GameSettings": [self.exeDirectory().absoluteFilePath("GameSettings")], + } + + def activeProblems(self) -> list[int]: + if self._organizer.managedGame() == self: + problems: set[Problems] = set() + # The dwmapi.dll loader should not be used with USVFS + ue4ss_loader = QFileInfo(self.exeDirectory().absoluteFilePath("dwmapi.dll")) + if ue4ss_loader.exists(): + problems.add(Problems.UE4SS_LOADER) + # Leverage UE4SS mod tab to find mod names with spaces + for mod in self._ue4ss_tab.get_mod_list(): + if " " in mod: + problems.add(Problems.INVALID_UE4SS_MOD_NAME) + break + return list(problems) + return [] + + def fullDescription(self, key: int) -> str: + match key: + case Problems.UE4SS_LOADER: + return ( + "The UE4SS loader DLL is present (dwmapi.dll). This will not function out-of-the box with MO2's virtual filesystem.\n\n" + + "In order to resolve this, either delete the DLL and use the OBSE UE4SS Loader plugin, or rename " + + "the DLL (ex. 'ue4ss_loader.dll') and set it to force load with the game exe.\n\n" + + "Do this for any executable which runs the game, such as the OBSE64 loader." + ) + case Problems.INVALID_UE4SS_MOD_NAME: + return ( + "UE4SS mods do not load properly with spaces in the mod name. These are stripped when parsing mods.txt and then" + "fail to match up when parsing the mods.json. Simply remove the spaces and they should load correctly." + ) + case _: + return "" + + def hasGuidedFix(self, key: int) -> bool: + match key: + case Problems.UE4SS_LOADER: + return True + case Problems.INVALID_UE4SS_MOD_NAME: + return True + case _: + return False + + def shortDescription(self, key: int) -> str: + match key: + case Problems.UE4SS_LOADER: + return "The UE4SS loader DLL is present (dwmapi.dll)." + case Problems.INVALID_UE4SS_MOD_NAME: + return "A UE4SS mod name contains a space." + case _: + return "" + + def startGuidedFix(self, key: int) -> None: + match key: + case Problems.UE4SS_LOADER: + os.rename( + self.exeDirectory().absoluteFilePath("dwmapi.dll"), + self.exeDirectory().absoluteFilePath("ue4ss_loader.dll"), + ) + case Problems.INVALID_UE4SS_MOD_NAME: + for mod in self._organizer.modList().allMods(): + filetree = self._organizer.modList().getMod(mod).fileTree() + ue4ss_mod = filetree.find("UE4SS") + if not ue4ss_mod: + filetree.find( + "Root/OblivionRemastered/Binaries/Win64/ue4ss/Mods" + ) + if isinstance(ue4ss_mod, mobase.IFileTree): + for entry in ue4ss_mod: + if isinstance(entry, mobase.IFileTree) and entry.find( + "scripts/main.lua" + ): + if " " in entry.name(): + mod_dir = Path( + self._organizer.modList() + .getMod(mod) + .absolutePath() + ) + mod_path = mod_dir.joinpath(entry.path("/")) + fixed_path = mod_dir.joinpath( + cast(mobase.IFileTree, entry.parent()).path( + "/" + ), + entry.name().replace(" ", ""), + ) + try: + mod_path.rename(fixed_path) + self._organizer.modDataChanged( + self._organizer.modList().getMod(mod) + ) + self._ue4ss_tab.update_mod_files(mod) + except FileExistsError: + pass + except FileNotFoundError: + pass + for entry in self.ue4ssDirectory().entryInfoList( + QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot + ): + entry_dir = QDir(entry.absoluteFilePath()) + if QFileInfo( + entry_dir.absoluteFilePath("scripts/main.lua") + ).exists(): + if " " in entry_dir.dirName(): + dest = ( + entry_dir.absoluteFilePath("..") + + "/" + + entry_dir.dirName().replace(" ", "") + ) + try: + os.rename(entry_dir.absolutePath(), dest) + except FileExistsError: + pass + except FileNotFoundError: + pass + case _: + pass diff --git a/libs/basic_games/games/game_readyornot.py b/libs/basic_games/games/game_readyornot.py new file mode 100644 index 0000000..d436e38 --- /dev/null +++ b/libs/basic_games/games/game_readyornot.py @@ -0,0 +1,24 @@ +from ..basic_game import BasicGame + + +class ReadyOrNotGame(BasicGame): + Name = "Ready or Not Support Plugin" + Author = "Ra2-IFV" + Version = "0.0.0.1" + + GameName = "Ready or Not" + GameShortName = "readyornot" + GameNexusName = "readyornot" + GameValidShortNames = ["ron"] + # GameNexusId = "readyornot" + GameBinary = "ReadyOrNot/Binaries/Win64/ReadyOrNot-Win64-Shipping.exe" + GameLauncher = "ReadyOrNot.exe" + GameDataPath = "ReadyOrNot/Content/Paks" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/ReadyOrNot" + GameIniFiles = [ + "%GAME_DOCUMENTS%/Saved/Config/Windows/Game.ini", + "%GAME_DOCUMENTS%/Saved/Config/Windows/GameUserSettings.ini", + ] + GameSavesDirectory = "%USERPROFILE%/AppData/Local/ReadyOrNot/Saved/SaveGames" + GameSaveExtension = "sav" + GameSteamId = 1144200 diff --git a/libs/basic_games/games/game_schedule1.py b/libs/basic_games/games/game_schedule1.py new file mode 100644 index 0000000..8ed98d6 --- /dev/null +++ b/libs/basic_games/games/game_schedule1.py @@ -0,0 +1,112 @@ +import json +from pathlib import Path + +from PyQt6.QtCore import QDir + +import mobase + +from ..basic_features import BasicLocalSavegames, BasicModDataChecker, GlobPatterns +from ..basic_features.basic_save_game_info import ( + BasicGameSaveGame, + BasicGameSaveGameInfo, +) +from ..basic_game import BasicGame + + +def parse_schedule1_save_metadata(save_path: Path, save: mobase.ISaveGame): + metadata_file = save_path / "Game.json" + try: + with open(metadata_file) as file: + meta_data = json.load(file) + name = meta_data["OrganisationName"] + if name != (save_name := save.getName()): + name = f"{save_name} ({name})" + return { + "Name": name, + "Game version": meta_data["GameVersion"], + } + except (FileNotFoundError, json.JSONDecodeError): + return None + + +class Schedule1SaveGame(BasicGameSaveGame): + def getName(self) -> str: + metadata_file = self._filepath / "Game.json" + try: + with open(metadata_file) as file: + meta_data = json.load(file) + return meta_data["OrganisationName"] + except (FileNotFoundError, json.JSONDecodeError): + return ( + f"[{self.getSaveGroupIdentifier().rstrip('s')}] {self._filepath.stem}" + ) + + def getSaveGroupIdentifier(self) -> str: + return self._filepath.parent.name + + +class Schedule1Game(BasicGame): + Name = "Schedule I Support Plugin" + Author = "shellbj" + Version = "1.0.0" + + GameName = "Schedule I" + GameShortName = "scheduleI" + GameNexusName = "schedule1" + GameNexusId = 7381 + GameSteamId = 3164500 + + GameBinary = "Schedule I.exe" + GameValidShortNames = ["schedule1", "scheduleI"] + GameDataPath = "" + GameSavesDirectory = r"%USERPROFILE%/AppData/LocalLow/TVGS/Schedule I/Saves" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Schedule-I" + ) + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature( + BasicModDataChecker( + GlobPatterns( + unfold=[ + # Fixes bad packaging for select mods + "ModManager&PhoneApp", + "AutoClearCompletedDeals", + ], + valid=[ + "meta.ini", # Included in installed mod folder. + "Mods", + "Plugins", + "UserData", # might not be needed + ], + delete=[ + "*.md", + "icon.png", + "fomod", # not sure this is needed either + ], + move={ + "*.dll": "Mods/", + # If these even exisit outside of mod packs + "*.ini": "UserData/", + "*.cfg": "UserData/", + "*.json": "UserData/", + }, + ) + ) + ) + self._register_feature(BasicLocalSavegames(self)) + self._register_feature( + BasicGameSaveGameInfo( + None, # no snapshot to add to the widget + parse_schedule1_save_metadata, + ) + ) + return True + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + return [ + Schedule1SaveGame(path) + for path in Path(folder.absolutePath()).glob("*/SaveGame_[1-5]") + ] diff --git a/libs/basic_games/games/game_sekiroshadowsdietwice.py b/libs/basic_games/games/game_sekiroshadowsdietwice.py new file mode 100644 index 0000000..5e7aaf0 --- /dev/null +++ b/libs/basic_games/games/game_sekiroshadowsdietwice.py @@ -0,0 +1,14 @@ +from ..basic_game import BasicGame + + +class SekiroShadowsDieTwiceGame(BasicGame): + Name = "Sekiro: Shadows Die Twice Support Plugin" + Author = "Kane Dou" + Version = "1.0.0" + + GameName = "Sekiro: Shadows Die Twice" + GameShortName = "sekiro" + GameBinary = "sekiro.exe" + GameDataPath = "mods" + GameSaveExtension = "sl2" + GameSteamId = 814380 diff --git a/libs/basic_games/games/game_silenthill2remake.py b/libs/basic_games/games/game_silenthill2remake.py new file mode 100644 index 0000000..4683a5d --- /dev/null +++ b/libs/basic_games/games/game_silenthill2remake.py @@ -0,0 +1,113 @@ +from typing import Tuple + +import mobase + +from ..basic_features import BasicModDataChecker, GlobPatterns +from ..basic_game import BasicGame + + +class SilentHill2RemakeModDataChecker(BasicModDataChecker): + def __init__(self): + super().__init__( + GlobPatterns( + delete=[ + "*.txt", + "*.md", + "manifest.json", + "icon.png", + ], + ) + ) + self.mod_path = ["SHProto", "Content", "Paks", "~mod"] + self.mod_path_lower = [name.lower() for name in self.mod_path] + + def _find_tree( + self, filetree: mobase.IFileTree + ) -> Tuple[str | None, mobase.FileTreeEntry | None]: + """ + Search the given filetree for a directory name that matches any component + of self.mod_path (case-insensitive). + + Returns: + (prefix, entry) + prefix: The missing part before the match (e.g. 'SHProto/Content/') + entry: The IFileTree entry that matched (e.g. the 'Paks' directory) + (None, None) if nothing matches. + """ + for entry in filetree: + if not entry.isDir(): + continue + + name_lower = entry.name().lower() + for i, component in enumerate(self.mod_path_lower): + if name_lower == component: + # Build the prefix string for everything *before* this match + prefix_parts = self.mod_path[:i] + prefix = "/".join(prefix_parts) + ("/" if prefix_parts else "") + return (prefix, entry) + # No matches found + return (None, None) + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + # Check for fully valid layout + has_entry, _ = self._find_tree(filetree) + if has_entry is None: + # in this case we check to make sure there's a .pak file + for entry in filetree: + if entry.name().lower().endswith(".pak") and entry.isFile(): + return mobase.ModDataChecker.FIXABLE + elif has_entry == "": + return mobase.ModDataChecker.VALID + else: + return mobase.ModDataChecker.FIXABLE + + # Otherwise, not recognizable + return mobase.ModDataChecker.INVALID + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + filetree = super().fix(filetree) + prefix, item = self._find_tree(filetree) + if prefix is None: + foundAPak = False + # Move all top-level items to BepInEx/plugins/ + items_to_move = list(filetree) + for cur_item in items_to_move: + if cur_item.name().lower().endswith(".pak"): + foundAPak = True + filetree.move(cur_item, f"SHProto/Content/Paks/~mod/{cur_item.name()}") + # foundAPack MUST be true because if 'prefix' returned None then + # there must be a .pak file or dataLooksValid wouldn't have returned + # a FIXABLE. This is therefore just a sanity check + assert foundAPak + return filetree + elif prefix == "": + return filetree + else: + # if prefix is not None then item cannot be None + assert item is not None + filetree.move(item, f"{prefix}{item.name()}") + return filetree + + +class SilentHill2RemakeGame(BasicGame): + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(SilentHill2RemakeModDataChecker()) + return True + + Name = "Silent Hill 2 Remake Support Plugin" + Author = "HomerSimpleton Returns" + Version = "1.0" + + GameName = "Silent Hill 2 Remake" + GameShortName = "silenthill2" + GameNexusName = "silenthill2" + + GameBinary = "SHProto/Binaries/Win64/SHProto-Win64-Shipping.exe" + GameLauncher = "SHProto.exe" + GameDataPath = "%GAME_PATH%" + GameSupportURL = "https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/Game:-Silent-Hill-2-Remake" + + GameGogId = [1225972913, 2051029707] diff --git a/libs/basic_games/games/game_silksong.py b/libs/basic_games/games/game_silksong.py new file mode 100644 index 0000000..12aab55 --- /dev/null +++ b/libs/basic_games/games/game_silksong.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from pathlib import Path + +from PyQt6.QtCore import QDir + +import mobase + +from ..basic_features import BasicModDataChecker, GlobPatterns +from ..basic_features.basic_save_game_info import BasicGameSaveGame +from ..basic_game import BasicGame + + +class SilksongModDataChecker(BasicModDataChecker): + def __init__(self, patterns: GlobPatterns | None = None): + super().__init__( + GlobPatterns( + valid=[ + # BepInEx files go to root + "BepInEx", + "doorstop_config.ini", + "winhttp.dll", + ".doorstop_version", + ], + delete=[ + "*.txt", + "*.md", + "manifest.json", + "icon.png", + ], + ).merge(patterns or GlobPatterns()), + ) + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + # Check if it contains BepInEx folders/files - if so, keep structure + if self._has_bepinex_structure(filetree): + return self.VALID + + # Everything else needs to go to BepInEx/plugins/ + return self.FIXABLE + + def _has_bepinex_structure(self, filetree: mobase.IFileTree) -> bool: + """Check if the mod has BepInEx folder structure""" + for entry in filetree: + name = entry.name().lower() + if name in [ + "bepinex", + "doorstop_config.ini", + "winhttp.dll", + ".doorstop_version", + ]: + return True + return False + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + # First apply the basic fix (handles delete patterns) + filetree = super().fix(filetree) + + # If no BepInEx structure, move everything to BepInEx/plugins/ + if not self._has_bepinex_structure(filetree): + # Move all top-level items to BepInEx/plugins/ + items_to_move = list(filetree) + for item in items_to_move: + filetree.move(item, f"BepInEx/plugins/{item.name()}") + + return filetree + + +class SilksongGame(BasicGame): + Name = "Hollow Knight: Silksong Support Plugin" + Author = "Nikirack" + Version = "1.0.0" + + GameName = "Hollow Knight: Silksong" + GameShortName = "hollowknightsilksong" # Match the error message + GameNexusName = "hollowknightsilksong" + GameSteamId = 1030300 + GameBinary = r"Hollow Knight Silksong.exe" + GameDataPath = "" + GameSavesDirectory = ( + r"%USERPROFILE%\AppData\LocalLow\Team Cherry\Hollow Knight Silksong" + ) + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Hollow-Knight-Silksong" + ) + + _forced_libraries = ["winhttp.dll"] + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(SilksongModDataChecker()) + return True + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + saves: list[mobase.ISaveGame] = [] + save_dir = Path(folder.absolutePath()) + + if save_dir.exists(): + # Look for common save file patterns + for pattern in ["*.save", "user*.dat"]: + for save_file in save_dir.rglob(pattern): + saves.append(BasicGameSaveGame(save_file)) + + return saves + + def executables(self) -> list[mobase.ExecutableInfo]: + return [ + mobase.ExecutableInfo( + self.gameName(), + self.gameDirectory().absoluteFilePath(self.binaryName()), + ) + ] + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + return [ + mobase.ExecutableForcedLoadSetting(self.binaryName(), lib).withEnabled(True) + for lib in self._forced_libraries + ] diff --git a/libs/basic_games/games/game_sims4.py b/libs/basic_games/games/game_sims4.py new file mode 100644 index 0000000..e5e7fa2 --- /dev/null +++ b/libs/basic_games/games/game_sims4.py @@ -0,0 +1,153 @@ +from collections.abc import Callable +from enum import IntEnum +from re import match +from typing import Any, List, Set, cast + +from mobase import ( + FileTreeEntry, + IFileTree, + IOrganizer, + ModDataChecker, + ModDataContent, + ReleaseType, + VersionInfo, +) + +from ..basic_game import BasicGame + + +class Content(IntEnum): + PACKAGE = 0 + SCRIPT = 1 + + +class ValidationResult(IntEnum): + INVALID = 0 + VALID = 1 + FIXABLE = 2 + + +class TS4Game(BasicGame): + Name = "The Sims 4 Support Plugin" + Author = "R3z Shark, xieve" + GameName = "The Sims 4" + GameShortName = "thesims4" + GameBinary = "Game/Bin/TS4_x64.exe" + GameDataPath = "%DOCUMENTS%/Electronic Arts/The Sims 4/Mods" + GameSteamId = 1222670 + GameOriginManifestIds = ["OFB-EAST:109552677"] + GameOriginWatcherExecutables = ("TS4_x64.exe",) + + def version(self): + return VersionInfo(1, 0, 0, ReleaseType.FINAL) + + def documentsDirectory(self): + return self.dataDirectory() + + def init(self, organizer: IOrganizer): + if super().init(organizer): + self._register_feature(TS4ModDataChecker()) + self._register_feature(TS4ModDataContent()) + return True + return False + + +class TS4ModDataChecker(ModDataChecker): + # .package files are allowed at a maximum depth of 5 subfolders, script files can be at most one level deep + # The first capturing group lazily captures any parent folders exceeding that depth, see below + _fixableOrValid = r"(?i)^(.*?)((?:[^\\]+\\){0,5}[^\\]*\.package|(?:[^\\]+\\)?[^\\]*\.ts4script|(?:[^\\]+\\)scripts\\.*\.py)$" + + def dataLooksValid(self, filetree: IFileTree) -> ModDataChecker.CheckReturn: + return cast( + ModDataChecker.CheckReturn, + self._fixOrValidateTree(filetree, validateMode=True), + ) + + def fix(self, filetree: IFileTree) -> IFileTree | None: + return cast(IFileTree, self._fixOrValidateTree(filetree)) + + def _fixOrValidateTree( + self, + tree: IFileTree, + validateMode: bool = False, + ) -> IFileTree | ModDataChecker.CheckReturn: + validationResult = ValidationResult.INVALID + checkReturn = ModDataChecker.INVALID + walkReturn = IFileTree.CONTINUE + + def setValidationResult( + newResult: ValidationResult = ValidationResult.INVALID, + actionCallback: Callable[[], Any] = lambda: None, + ): + nonlocal validationResult + nonlocal checkReturn + nonlocal walkReturn + if validateMode: + validationResult = max(validationResult, newResult) + match validationResult: + case ValidationResult.INVALID: + pass + case ValidationResult.VALID: + checkReturn = ModDataChecker.VALID + case ValidationResult.FIXABLE: + checkReturn = ModDataChecker.FIXABLE + walkReturn = IFileTree.STOP + else: + actionCallback() + + def fixOrValidateEntry( + parentPath: str, entry: FileTreeEntry + ) -> IFileTree.WalkReturn: + path = f"{parentPath}{entry.name()}" + fixableOrValid = match(self._fixableOrValid, path) + if fixableOrValid: + match fixableOrValid.groups(): + case ["", _]: + setValidationResult(ValidationResult.VALID) + case [_, innerPath]: + # Move files that are nested too deeply up, preserving the inner folder structure + # E.g. a/b/c.ts4script will be moved to b/c.ts4script + setValidationResult( + ValidationResult.FIXABLE, + lambda: tree.move(entry, innerPath), + ) + case _: + pass + return walkReturn + + tree.walk(fixOrValidateEntry) + + if validateMode: + return checkReturn + else: + return tree + + +class TS4ModDataContent(ModDataContent): + def getAllContents(self: ModDataContent) -> List[ModDataContent.Content]: + return [ + ModDataContent.Content( + Content.PACKAGE, "Package", ":/MO/gui/content/plugin" + ), + ModDataContent.Content(Content.SCRIPT, "Script", ":/MO/gui/content/script"), + ] + + def getContentsFor(self: ModDataContent, filetree: IFileTree) -> List[int]: + contents: Set[int] = set() + + def getContentForEntry(path: str, entry: FileTreeEntry): + nonlocal contents + match entry.suffix(): + case "package": + contents.add(Content.PACKAGE) + case "ts4script" | "py": + contents.add(Content.SCRIPT) + case _: + pass + if len(contents) == 2: + return IFileTree.STOP + else: + return IFileTree.CONTINUE + + filetree.walk(getContentForEntry) + return list(contents) diff --git a/libs/basic_games/games/game_stalkeranomaly.py b/libs/basic_games/games/game_stalkeranomaly.py new file mode 100644 index 0000000..bb40575 --- /dev/null +++ b/libs/basic_games/games/game_stalkeranomaly.py @@ -0,0 +1,293 @@ +from enum import IntEnum +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo, Qt +from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget + +import mobase + +from ..basic_features.basic_save_game_info import ( + BasicGameSaveGame, + BasicGameSaveGameInfo, +) +from ..basic_game import BasicGame +from .stalkeranomaly import XRSave + + +class StalkerAnomalyModDataChecker(mobase.ModDataChecker): + _valid_folders: list[str] = [ + "appdata", + "bin", + "db", + "gamedata", + ] + + def hasValidFolders(self, tree: mobase.IFileTree) -> bool: + for e in tree: + if e.isDir(): + if e.name().lower() in self._valid_folders: + return True + + return False + + def findLostData(self, tree: mobase.IFileTree) -> list[mobase.FileTreeEntry]: + lost_db: list[mobase.FileTreeEntry] = [] + + for e in tree: + if e.isFile(): + if e.suffix().lower().startswith("db"): + lost_db.append(e) + + return lost_db + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + if self.hasValidFolders(filetree): + return mobase.ModDataChecker.VALID + + if self.findLostData(filetree): + return mobase.ModDataChecker.FIXABLE + + return mobase.ModDataChecker.INVALID + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + lost_db = self.findLostData(filetree) + if lost_db: + rfolder = filetree.addDirectory("db").addDirectory("mods") + for r in lost_db: + rfolder.insert(r, mobase.IFileTree.REPLACE) + + return filetree + + +class Content(IntEnum): + INTERFACE = 0 + TEXTURE = 1 + MESH = 2 + SCRIPT = 3 + SOUND = 4 + MCM = 5 + CONFIG = 6 + + +class StalkerAnomalyModDataContent(mobase.ModDataContent): + content: list[int] = [] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [ + mobase.ModDataContent.Content( + Content.INTERFACE, "Interface", ":/MO/gui/content/interface" + ), + mobase.ModDataContent.Content( + Content.TEXTURE, "Textures", ":/MO/gui/content/texture" + ), + mobase.ModDataContent.Content( + Content.MESH, "Meshes", ":/MO/gui/content/mesh" + ), + mobase.ModDataContent.Content( + Content.SCRIPT, "Scripts", ":/MO/gui/content/script" + ), + mobase.ModDataContent.Content( + Content.SOUND, "Sounds", ":/MO/gui/content/sound" + ), + mobase.ModDataContent.Content(Content.MCM, "MCM", ":/MO/gui/content/menu"), + mobase.ModDataContent.Content( + Content.CONFIG, "Configs", ":/MO/gui/content/inifile" + ), + ] + + def walkContent( + self, path: str, entry: mobase.FileTreeEntry + ) -> mobase.IFileTree.WalkReturn: + name = entry.name().lower() + if entry.isFile(): + ext = entry.suffix().lower() + if ext in ["dds", "thm"]: + self.content.append(Content.TEXTURE) + if path.startswith("gamedata/textures/ui"): + self.content.append(Content.INTERFACE) + elif ext in ["omf", "ogf"]: + self.content.append(Content.MESH) + elif ext in ["script"]: + self.content.append(Content.SCRIPT) + if "_mcm" in name: + self.content.append(Content.MCM) + elif ext in ["ogg"]: + self.content.append(Content.SOUND) + elif ext in ["ltx", "xml"]: + self.content.append(Content.CONFIG) + if path.startswith("gamedata/configs/ui"): + self.content.append(Content.INTERFACE) + + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.content = [] + filetree.walk(self.walkContent, "/") + return self.content + + +class StalkerAnomalySaveGame(BasicGameSaveGame): + _filepath: Path + + xr_save: XRSave + + def __init__(self, filepath: Path): + super().__init__(filepath) + self._filepath = filepath + self.xr_save = XRSave(self._filepath) + + def getName(self) -> str: + xr_save = self.xr_save + player = xr_save.player + if player: + name = player.character_name_str + time = xr_save.time_fmt + return f"{name}, {xr_save.save_fmt} [{time}]" + return "" + + def allFiles(self) -> list[str]: + filepath = str(self._filepath) + paths = [filepath] + scoc = filepath.replace(".scop", ".scoc") + if Path(scoc).exists(): + paths.append(scoc) + dds = filepath.replace(".scop", ".dds") + if Path(dds).exists(): + paths.append(dds) + return paths + + +class StalkerAnomalySaveGameInfoWidget(mobase.ISaveGameInfoWidget): + def __init__(self, parent: QWidget | None): + super().__init__(parent) + layout = QVBoxLayout() + self._labelSave = self.newLabel(layout) + self._labelName = self.newLabel(layout) + self._labelFaction = self.newLabel(layout) + self._labelHealth = self.newLabel(layout) + self._labelMoney = self.newLabel(layout) + self._labelRank = self.newLabel(layout) + self._labelRep = self.newLabel(layout) + self.setLayout(layout) + palette = self.palette() + palette.setColor(self.backgroundRole(), Qt.GlobalColor.black) + self.setAutoFillBackground(True) + self.setPalette(palette) + self.setWindowFlags( + Qt.WindowType.ToolTip | Qt.WindowType.BypassGraphicsProxyWidget + ) + + def newLabel(self, layout: QVBoxLayout) -> QLabel: + label = QLabel() + label.setAlignment(Qt.AlignmentFlag.AlignLeft) + palette = label.palette() + palette.setColor(label.foregroundRole(), Qt.GlobalColor.white) + label.setPalette(palette) + layout.addWidget(label) + layout.addStretch() + return label + + def setSave(self, save: mobase.ISaveGame): + self.resize(240, 32) + if not isinstance(save, StalkerAnomalySaveGame): + return + xr_save = save.xr_save + player = xr_save.player + if player: + self._labelSave.setText(f"Save: {xr_save.save_fmt}") + self._labelName.setText(f"Name: {player.character_name_str}") + self._labelFaction.setText(f"Faction: {xr_save.getFaction()}") + self._labelHealth.setText(f"Health: {player.health:.2f}%") + self._labelMoney.setText(f"Money: {player.money} RU") + self._labelRank.setText(f"Rank: {xr_save.getRank()} ({player.rank})") + self._labelRep.setText( + f"Reputation: {xr_save.getReputation()} ({player.reputation})" + ) + + +class StalkerAnomalySaveGameInfo(BasicGameSaveGameInfo): + def getSaveGameWidget(self, parent: QWidget | None = None): + return StalkerAnomalySaveGameInfoWidget(parent) + + +class StalkerAnomalyGame(BasicGame, mobase.IPluginFileMapper): + Name = "STALKER Anomaly" + Author = "Qudix" + Version = "0.5.0" + Description = "Adds support for STALKER Anomaly" + + GameName = "STALKER Anomaly" + GameShortName = "stalkeranomaly" + GameNexusName = "stalkeranomaly" + GameNexusId = 3743 + GameBinary = "AnomalyLauncher.exe" + GameDataPath = "" + GameDocumentsDirectory = "%GAME_PATH%/appdata" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-S.T.A.L.K.E.R.-Anomaly" + ) + + GameSaveExtension = "scop" + GameSavesDirectory = "%GAME_DOCUMENTS%/savedgames" + + def __init__(self): + BasicGame.__init__(self) + mobase.IPluginFileMapper.__init__(self) + + def init(self, organizer: mobase.IOrganizer): + BasicGame.init(self, organizer) + self._register_feature(StalkerAnomalyModDataChecker()) + self._register_feature(StalkerAnomalyModDataContent()) + self._register_feature(StalkerAnomalySaveGameInfo()) + organizer.onAboutToRun(lambda _str: self.aboutToRun(_str)) + return True + + def aboutToRun(self, _str: str) -> bool: + gamedir = self.gameDirectory() + if gamedir.exists(): + # For mappings + gamedir.mkdir("appdata") + # The game will crash if this file exists in the + # virtual tree rather than the game dir + dbg_path = Path(self._gamePath, "gamedata/configs/cache_dbg.ltx") + if not dbg_path.exists(): + dbg_path.parent.mkdir(parents=True, exist_ok=True) + with open(dbg_path, "w", encoding="utf-8"): + pass + return True + + def executables(self) -> list[mobase.ExecutableInfo]: + info = [ + ["Anomaly Launcher", "AnomalyLauncher.exe"], + ["Anomaly (DX11-AVX)", "bin/AnomalyDX11AVX.exe"], + ["Anomaly (DX11)", "bin/AnomalyDX11.exe"], + ["Anomaly (DX10-AVX)", "bin/AnomalyDX10AVX.exe"], + ["Anomaly (DX10)", "bin/AnomalyDX10.exe"], + ["Anomaly (DX9-AVX)", "bin/AnomalyDX9AVX.exe"], + ["Anomaly (DX9)", "bin/AnomalyDX9.exe"], + ["Anomaly (DX8-AVX)", "bin/AnomalyDX8AVX.exe"], + ["Anomaly (DX8)", "bin/AnomalyDX8.exe"], + ] + gamedir = self.gameDirectory() + return [ + mobase.ExecutableInfo(inf[0], QFileInfo(gamedir, inf[1])) for inf in info + ] + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + StalkerAnomalySaveGame(path) + for path in Path(folder.absolutePath()).glob(f"*.{ext}") + ] + + def mappings(self) -> list[mobase.Mapping]: + appdata = self.gameDirectory().filePath("appdata") + m = mobase.Mapping() + m.createTarget = True + m.isDirectory = True + m.source = appdata + m.destination = appdata + return [m] diff --git a/libs/basic_games/games/game_stardewvalley.py b/libs/basic_games/games/game_stardewvalley.py new file mode 100644 index 0000000..6397b40 --- /dev/null +++ b/libs/basic_games/games/game_stardewvalley.py @@ -0,0 +1,57 @@ +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class StardewValleyModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + for e in filetree: + if isinstance(e, mobase.IFileTree) and e.exists( + "manifest.json", mobase.IFileTree.FILE + ): + return mobase.ModDataChecker.VALID + + return mobase.ModDataChecker.INVALID + + +class StardewValleyGame(BasicGame): + Name = "Stardew Valley Support Plugin" + Author = "Syer10" + Version = "0.1.0a" + + GameName = "Stardew Valley" + GameShortName = "stardewvalley" + GameNexusName = "stardewvalley" + GameNexusId = 1303 + GameSteamId = 413150 + GameGogId = 1453375253 + GameBinary = "Stardew Valley.exe" + GameDataPath = "mods" + GameDocumentsDirectory = "%DOCUMENTS%/StardewValley" + GameSavesDirectory = "%GAME_DOCUMENTS%/Saves" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Stardew-Valley" + ) + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._register_feature(StardewValleyModDataChecker()) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "SMAPI", QFileInfo(self.gameDirectory(), "StardewModdingAPI.exe") + ), + mobase.ExecutableInfo( + "Stardew Valley", QFileInfo(self.gameDirectory(), "Stardew Valley.exe") + ), + ] diff --git a/libs/basic_games/games/game_starsector.py b/libs/basic_games/games/game_starsector.py new file mode 100644 index 0000000..2dc27d9 --- /dev/null +++ b/libs/basic_games/games/game_starsector.py @@ -0,0 +1,30 @@ +from pathlib import Path + +from PyQt6.QtCore import QDir + +import mobase + +from ..basic_game import BasicGame, BasicGameSaveGame + + +class Starsector(BasicGame): + Name = "Starsector Support Plugin" + Author = "ddbb07" + Version = "1.0.1" + + GameName = "Starsector" + GameShortName = "starsector" + GameNexusName = "starsector" + GameBinary = "starsector.exe" + GameDataPath = "mods" + GameSavesDirectory = "%GAME_PATH%/saves" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Starsector" + ) + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + return [ + BasicGameSaveGame(path) + for path in Path(folder.absolutePath()).glob("save_*") + ] diff --git a/libs/basic_games/games/game_starwars-empire-at-war-foc.py b/libs/basic_games/games/game_starwars-empire-at-war-foc.py new file mode 100644 index 0000000..188c288 --- /dev/null +++ b/libs/basic_games/games/game_starwars-empire-at-war-foc.py @@ -0,0 +1,33 @@ +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class StarWarsEmpireAtWarGame(BasicGame): + Name = "STAR WARS Empire at War - Force of Corruption" + Author = "erri120" + Version = "1.0.0" + + GameName = "STAR WARS™ Empire at War: Forces of Corruption" + GameShortName = "starwarsempireatwar" + GameNexusName = "starwarsempireatwar" + GameNexusId = 453 + GameSteamId = 32470 + GameGogId = 1421404887 + # using StarWarsG.exe instead of swfoc.exe because it has an icon + GameBinary = "corruption/StarWarsG.exe" + GameDataPath = "corruption/Data" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Star-Wars:-Empire-At-War" + ) + + def executables(self) -> list[mobase.ExecutableInfo]: + return [ + mobase.ExecutableInfo( + "STAR WARS Empire at War: Forces of Corruption", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] diff --git a/libs/basic_games/games/game_starwars-empire-at-war.py b/libs/basic_games/games/game_starwars-empire-at-war.py new file mode 100644 index 0000000..171ecfc --- /dev/null +++ b/libs/basic_games/games/game_starwars-empire-at-war.py @@ -0,0 +1,33 @@ +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class StarWarsEmpireAtWarGame(BasicGame): + Name = "STAR WARS Empire at War" + Author = "erri120" + Version = "1.0.0" + + GameName = "STAR WARS™ Empire at War" + GameShortName = "starwarsempireatwar" + GameNexusName = "starwarsempireatwar" + GameNexusId = 453 + GameSteamId = 32470 + GameGogId = 1421404887 + # using StarWarsG.exe instead of sweaw.exe because it has an icon + GameBinary = "GameData/StarWarsG.exe" + GameDataPath = "GameData/Data" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Star-Wars:-Empire-At-War" + ) + + def executables(self) -> list[mobase.ExecutableInfo]: + return [ + mobase.ExecutableInfo( + "STAR WARS Empire at War", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] diff --git a/libs/basic_games/games/game_subnautica-below-zero.py b/libs/basic_games/games/game_subnautica-below-zero.py new file mode 100644 index 0000000..297049f --- /dev/null +++ b/libs/basic_games/games/game_subnautica-below-zero.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from ..basic_features import GlobPatterns +from . import game_subnautica # namespace to not load SubnauticaGame here, too! + + +class SubnauticaBelowZeroGame(game_subnautica.SubnauticaGame): + Name = "Subnautica Below Zero Support Plugin" + + GameName = "Subnautica: Below Zero" + GameShortName = "subnauticabelowzero" + GameNexusName = "subnauticabelowzero" + GameThunderstoreName = "subnautica-below-zero" + GameSteamId = 848450 + GameEpicId = "foxglove" + GameBinary = "SubnauticaZero.exe" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Subnautica:-Below-Zero" + ) + + _game_extra_save_paths = [ + r"%USERPROFILE%\Appdata\LocalLow\Unknown Worlds" + r"\Subnautica Below Zero\SubnauticaZero\SavedGames" + ] + + def _set_mod_data_checker( + self, extra_patterns: GlobPatterns | None = None, use_qmod: bool | None = None + ): + super()._set_mod_data_checker( + GlobPatterns(unfold=["BepInExPack_BelowZero"]), use_qmod + ) diff --git a/libs/basic_games/games/game_subnautica.py b/libs/basic_games/games/game_subnautica.py new file mode 100644 index 0000000..f9e5128 --- /dev/null +++ b/libs/basic_games/games/game_subnautica.py @@ -0,0 +1,279 @@ +from __future__ import annotations + +import fnmatch +import os +from collections.abc import Iterable +from enum import Enum +from pathlib import Path + +from PyQt6.QtCore import QDir, qWarning + +import mobase + +from ..basic_features import BasicModDataChecker, GlobPatterns +from ..basic_features.basic_save_game_info import ( + BasicGameSaveGame, + BasicGameSaveGameInfo, +) +from ..basic_features.utils import is_directory +from ..basic_game import BasicGame + + +class SubnauticaModDataChecker(BasicModDataChecker): + use_qmods: bool = False + + def __init__(self, patterns: GlobPatterns | None = None, use_qmods: bool = False): + super().__init__( + GlobPatterns( + unfold=["BepInExPack_Subnautica"], + valid=[ + "BepInEx", + "doorstop_libs", + "doorstop_config.ini", + "run_bepinex.sh", + "winhttp.dll", + "QMods", + ".doorstop_version", # Added in Tobey's BepInEx Pack for Subnautica v5.4.23 + "changelog.txt", + "libdoorstop.dylib", + ], + ignore=["*.mohidden"], + delete=[ + "*.txt", + "*.md", + "icon.png", + "license", + "manifest.json", + ], + move={ + "plugins": "BepInEx/", + "patchers": "BepInEx/", + "CustomCraft2SML": "QMods/" if use_qmods else "BepInEx/plugins/", + "CustomCraft3": "QMods/" if use_qmods else "BepInEx/plugins/", + }, + ).merge(patterns or GlobPatterns()), + ) + self.use_qmods = use_qmods + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + # fix: single root folders get traversed by Simple Installer + parent = filetree.parent() + if parent is not None and self.dataLooksValid(parent) is self.FIXABLE: + return self.FIXABLE + check_return = super().dataLooksValid(filetree) + # A single unknown folder with a dll file in is to be moved to BepInEx/plugins/ + if ( + check_return is self.INVALID + and len(filetree) == 1 + and is_directory(folder := filetree[0]) + and any(fnmatch.fnmatch(entry.name(), "*.dll") for entry in folder) + ): + return self.FIXABLE + return check_return + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + filetree = super().fix(filetree) + if ( + self.dataLooksValid(filetree) is self.FIXABLE + and len(filetree) == 1 + and is_directory(folder := filetree[0]) + and any(fnmatch.fnmatch(entry.name(), "*.dll") for entry in folder) + ): + filetree.move(folder, "QMods/" if self.use_qmods else "BepInEx/plugins/") + return filetree + + +class SubnauticaGame(BasicGame, mobase.IPluginFileMapper): + Name = "Subnautica Support Plugin" + Author = "dekart811, Zash" + Version = "2.3" + + GameName = "Subnautica" + GameShortName = "subnautica" + GameNexusName = "subnautica" + GameThunderstoreName = "subnautica" + GameSteamId = 264710 + GameEpicId = "Jaguar" + GameBinary = "Subnautica.exe" + GameDataPath = "_ROOT" # Custom mappings to actual root folders below. + GameDocumentsDirectory = r"%GAME_PATH%" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Subnautica" + ) + GameSavesDirectory = r"%GAME_PATH%\SNAppData\SavedGames" + + _game_extra_save_paths = [ + r"%USERPROFILE%\Appdata\LocalLow\Unknown Worlds" + r"\Subnautica\Subnautica\SavedGames" + ] + + _forced_libraries = ["winhttp.dll"] + + _root_blacklist = {GameDataPath.casefold()} + + class MapType(Enum): + FILE = 0 + FOLDER = 1 + + _root_extra_overwrites: dict[str, MapType] = { + "qmodmanager_log-Subnautica.txt": MapType.FILE, + "qmodmanager-config.json": MapType.FILE, + "BepInEx_Shim_Backup": MapType.FOLDER, + } + """Extra files & folders created in game root by mods / BepInEx after game launch, + but not included in the mod archives. + """ + + def __init__(self): + super().__init__() + mobase.IPluginFileMapper.__init__(self) + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._set_mod_data_checker() + self._register_feature( + BasicGameSaveGameInfo(lambda s: Path(s or "", "screenshot.jpg")) + ) + + organizer.onPluginSettingChanged(self._settings_change_callback) + return True + + def _set_mod_data_checker( + self, extra_patterns: GlobPatterns | None = None, use_qmod: bool | None = None + ): + self._register_feature( + SubnauticaModDataChecker( + patterns=(GlobPatterns() if extra_patterns is None else extra_patterns), + use_qmods=( + bool(self._organizer.pluginSetting(self.name(), "use_qmods")) + if use_qmod is None + else use_qmod + ), + ) + ) + + def _settings_change_callback( + self, + plugin_name: str, + setting: str, + old: mobase.MoVariant, + new: mobase.MoVariant, + ): + if plugin_name == self.name() and setting == "use_qmods": + self._set_mod_data_checker(use_qmod=bool(new)) + + def settings(self) -> list[mobase.PluginSetting]: + return [ + mobase.PluginSetting( + "use_qmods", + ( + "Install */.dll mods in legacy QMods folder," + " instead of BepInEx/plugins (default)." + ), + default_value=False, + ) + ] + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + return [ + BasicGameSaveGame(folder) + for save_path in ( + folder.absolutePath(), + *(os.path.expandvars(p) for p in self._game_extra_save_paths), + ) + for folder in Path(save_path).glob("slot*") + ] + + def executables(self) -> list[mobase.ExecutableInfo]: + binary = self.gameDirectory().absoluteFilePath(self.binaryName()) + return [ + mobase.ExecutableInfo( + self.gameName(), + binary, + ).withArgument("-vrmode none"), + mobase.ExecutableInfo( + f"{self.gameName()} VR", + self.gameDirectory().absoluteFilePath(self.binaryName()), + ), + ] + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + return [ + mobase.ExecutableForcedLoadSetting(self.binaryName(), lib).withEnabled(True) + for lib in self._forced_libraries + ] + + def mappings(self) -> list[mobase.Mapping]: + game = self._organizer.managedGame() + game_path = Path(game.gameDirectory().absolutePath()) + overwrite_path = Path(self._organizer.overwritePath()) + + return [ + *( + # Extra overwrites + self._overwrite_mapping( + overwrite_path / name, + dest, + is_dir=(map_type is self.MapType.FOLDER), + ) + for name, map_type in self._root_extra_overwrites.items() + if not (dest := game_path / name).exists() + ), + *self._root_mappings(game_path, overwrite_path), + ] + + def _root_mappings( + self, game_path: Path, overwrite_path: Path + ) -> Iterable[mobase.Mapping]: + for mod_path in self._active_mod_paths(): + mod_name = mod_path.name + + for child in mod_path.iterdir(): + # Check blacklist + if child.name.casefold() in self._root_blacklist: + qWarning(f"Skipping {child.name} ({mod_name})") + continue + destination = game_path / child.name + # Check existing + if destination.exists(): + qWarning( + f"Overwriting of existing game files/folders is not supported! " + f"{destination.as_posix()} ({mod_name})" + ) + continue + # Mapping: mod -> root + yield mobase.Mapping( + source=str(child), + destination=str(destination), + is_directory=child.is_dir(), + create_target=False, + ) + if child.is_dir(): + # Mapping: overwrite <-> root + yield self._overwrite_mapping( + overwrite_path / child.name, destination, is_dir=True + ) + + def _active_mod_paths(self) -> Iterable[Path]: + mods_parent_path = Path(self._organizer.modsPath()) + modlist = self._organizer.modList().allModsByProfilePriority() + for mod in modlist: + if self._organizer.modList().state(mod) & mobase.ModState.ACTIVE: + yield mods_parent_path / mod + + def _overwrite_mapping( + self, overwrite_source: Path, destination: Path, is_dir: bool + ) -> mobase.Mapping: + """Mapping: overwrite <-> root""" + if is_dir: + # Root folders in overwrite need to exits. + overwrite_source.mkdir(parents=True, exist_ok=True) + return mobase.Mapping( + str(overwrite_source), + str(destination), + is_dir, + create_target=True, + ) diff --git a/libs/basic_games/games/game_tdu.py b/libs/basic_games/games/game_tdu.py new file mode 100644 index 0000000..5624ff8 --- /dev/null +++ b/libs/basic_games/games/game_tdu.py @@ -0,0 +1,36 @@ +# -*- encoding: utf-8 -*- + +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class TDUGame(BasicGame): + Name = "Test Drive Unlimited Support Plugin" + Author = "uwx" + Version = "1.0.0" + + GameName = "Test Drive Unlimited" + GameShortName = "tdu" + GameNexusName = "testdriveunlimited" + GameNexusId = 4615 + GameBinary = "TestDriveUnlimited.exe" + GameDataPath = "" + + def executables(self): + return [ + mobase.ExecutableInfo( + "Test Drive Unlimited", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("-w -wx -vsync -bigbnks -offline"), + mobase.ExecutableInfo( + "Project Paradise Launcher", + QFileInfo( + self.gameDirectory().absoluteFilePath( + "TDU - Project Paradise Launcher.exe" + ) + ), + ), + ] diff --git a/libs/basic_games/games/game_tdu2.py b/libs/basic_games/games/game_tdu2.py new file mode 100644 index 0000000..f7fd94f --- /dev/null +++ b/libs/basic_games/games/game_tdu2.py @@ -0,0 +1,35 @@ +# -*- encoding: utf-8 -*- + +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class TDU2Game(BasicGame): + Name = "Test Drive Unlimited 2 Support Plugin" + Author = "uwx" + Version = "1.0.0" + + GameName = "Test Drive Unlimited 2" + GameShortName = "tdu2" + GameNexusName = "testdriveunlimited2" + GameNexusId = 2353 + GameSteamId = 9930 + GameBinary = "UpLauncher.exe" + GameDataPath = "" + + def executables(self): + return [ + mobase.ExecutableInfo( + "Test Drive Unlimited 2", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "TDU2 Universal Launcher", + QFileInfo( + self.gameDirectory().absoluteFilePath("TDU2_Universal_Launcher.exe") + ), + ), + ] diff --git a/libs/basic_games/games/game_thebindingofisaacrebirth.py b/libs/basic_games/games/game_thebindingofisaacrebirth.py new file mode 100644 index 0000000..a21f11d --- /dev/null +++ b/libs/basic_games/games/game_thebindingofisaacrebirth.py @@ -0,0 +1,23 @@ +from ..basic_game import BasicGame + + +class TheBindingOfIsaacRebirthGame(BasicGame): + Name = "The Binding of Isaac: Rebirth - Support Plugin" + Author = "Luca/EzioTheDeadPoet" + Version = "0.1.0" + + GameName = "The Binding of Isaac: Rebirth" + GameShortName = "thebindingofisaacrebirth" + GameNexusName = "thebindingofisaacrebirth" + GameNexusId = 1293 + GameSteamId = 250900 + GameBinary = "isaac-ng.exe" + GameDocumentsDirectory = "%DOCUMENTS%/My Games/Binding of Isaac Afterbirth+" + GameDataPath = "%DOCUMENTS%/My Games/Binding of Isaac Afterbirth+ Mods" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-The-Binding-of-Isaac:-Rebirth" + ) + + def iniFiles(self): + return ["options.ini"] diff --git a/libs/basic_games/games/game_thps3.py b/libs/basic_games/games/game_thps3.py new file mode 100644 index 0000000..52ea07c --- /dev/null +++ b/libs/basic_games/games/game_thps3.py @@ -0,0 +1,38 @@ +# -*- encoding: utf-8 -*- + +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class THPS3Game(BasicGame): + Name = "Tony Hawk's Pro Skater 3 Support Plugin" + Author = "uwx" + Version = "1.0.0" + + GameName = "Tony Hawk's Pro Skater 3" + GameShortName = "thps3" + GameBinary = "Skate3.exe" + GameDataPath = "Data" + + def executables(self): + return [ + mobase.ExecutableInfo( + "Tony Hawk's Pro Skater 3", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "Tony Hawk's Pro Skater 3 Setup", + QFileInfo(self.gameDirectory().absoluteFilePath("THPS3Setup.exe")), + ), + mobase.ExecutableInfo( + "Tony Hawk's Pro Skater 3 (PARTYMOD)", + QFileInfo(self.gameDirectory().absoluteFilePath("THPS3.exe")), + ), + mobase.ExecutableInfo( + "PARTYMOD Configurator", + QFileInfo(self.gameDirectory().absoluteFilePath("partyconfig.exe")), + ), + ] diff --git a/libs/basic_games/games/game_thps4.py b/libs/basic_games/games/game_thps4.py new file mode 100644 index 0000000..7a7ca0b --- /dev/null +++ b/libs/basic_games/games/game_thps4.py @@ -0,0 +1,40 @@ +# -*- encoding: utf-8 -*- + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class THPS4Game(BasicGame): + Name = "Tony Hawk's Pro Skater 4 Support Plugin" + Author = "uwx" + Version = "1.0.0" + + GameName = "Tony Hawk's Pro Skater 4" + GameShortName = "thps4" + GameBinary = "Skate4.exe" + GameDataPath = "Data" + + def executables(self): + return [ + mobase.ExecutableInfo( + "Tony Hawk's Pro Skater 4", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "Tony Hawk's Pro Skater 4 Setup", + QFileInfo(self.gameDirectory().absoluteFilePath("../Start.exe")), + ).withWorkingDirectory( + QDir(QDir.cleanPath(self.gameDirectory().absoluteFilePath(".."))) + ), + mobase.ExecutableInfo( + "Tony Hawk's Pro Skater 4 (PARTYMOD)", + QFileInfo(self.gameDirectory().absoluteFilePath("THPS4.exe")), + ), + mobase.ExecutableInfo( + "PARTYMOD Configurator", + QFileInfo(self.gameDirectory().absoluteFilePath("partyconfig.exe")), + ), + ] diff --git a/libs/basic_games/games/game_thug.py b/libs/basic_games/games/game_thug.py new file mode 100644 index 0000000..6a0976f --- /dev/null +++ b/libs/basic_games/games/game_thug.py @@ -0,0 +1,36 @@ +# -*- encoding: utf-8 -*- + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class THPS4Game(BasicGame): + Name = "Tony Hawk's Underground Support Plugin" + Author = "uwx" + Version = "1.0.0" + + GameName = "Tony Hawk's Underground" + GameShortName = "thug" + GameBinary = "THUG.exe" + GameDataPath = "Data" + + def executables(self): + return [ + mobase.ExecutableInfo( + "Tony Hawk's Underground", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "Tony Hawk's Underground Launcher", + QFileInfo(self.gameDirectory().absoluteFilePath("../Launcher.exe")), + ).withWorkingDirectory( + QDir(QDir.cleanPath(self.gameDirectory().absoluteFilePath(".."))) + ), + mobase.ExecutableInfo( + "Tony Hawk's Underground (ClownJob'd)", + QFileInfo(self.gameDirectory().absoluteFilePath("THUGONE.exe")), + ), + ] diff --git a/libs/basic_games/games/game_thug2.py b/libs/basic_games/games/game_thug2.py new file mode 100644 index 0000000..389e204 --- /dev/null +++ b/libs/basic_games/games/game_thug2.py @@ -0,0 +1,54 @@ +# -*- encoding: utf-8 -*- + +import os + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class THPS4Game(BasicGame): + Name = "Tony Hawk's Underground 2 Support Plugin" + Author = "uwx" + Version = "1.0.0" + + GameName = "Tony Hawk's Underground 2" + GameShortName = "thug2" + GameBinary = "THUG2.exe" + GameDataPath = "Data" + + def executables(self): + return [ + mobase.ExecutableInfo( + "Tony Hawk's Underground 2", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "Tony Hawk's Underground 2 Launcher", + QFileInfo(self.gameDirectory().absoluteFilePath("../Launcher.exe")), + ).withWorkingDirectory( + QDir(QDir.cleanPath(self.gameDirectory().absoluteFilePath(".."))) + ), + mobase.ExecutableInfo( + "Tony Hawk's Underground 2 (ClownJob'd)", + QFileInfo(self.gameDirectory().absoluteFilePath("THUGTWO.exe")), + ), + mobase.ExecutableInfo( + "THUG Pro Launcher", + QFileInfo( + QDir(os.getenv("LOCALAPPDATA")).absoluteFilePath( + "THUG Pro/THUGProLauncher.exe" + ) + ), + ), + mobase.ExecutableInfo( + "THUG Pro", + QFileInfo( + QDir(os.getenv("LOCALAPPDATA")).absoluteFilePath( + "THUG Pro/THUGPro.exe" + ) + ), + ), + ] diff --git a/libs/basic_games/games/game_tmuf.py b/libs/basic_games/games/game_tmuf.py new file mode 100644 index 0000000..648da6d --- /dev/null +++ b/libs/basic_games/games/game_tmuf.py @@ -0,0 +1,35 @@ +# -*- encoding: utf-8 -*- + +from __future__ import annotations + +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class TmufGame(BasicGame): + Name = "Trackmania United Forever Support Plugin" + Author = "uwx" + Version = "1.0.0" + Description = "Adds support for Trackmania United Forever game folder mods." + + GameName = "Trackmania United Forever" + GameShortName = "tmuf" + GameNexusName = "trackmaniaunited" + GameNexusId = 1500 + GameSteamId = 7200 + GameBinary = "TmForeverLauncher.exe" + GameDataPath = "GameData" + + def executables(self): + return [ + mobase.ExecutableInfo( + "Trackmania United Forever", + QFileInfo( + self.gameDirectory(), + self.binaryName(), + ), + ), + ] diff --git a/libs/basic_games/games/game_trainsimulator.py b/libs/basic_games/games/game_trainsimulator.py new file mode 100644 index 0000000..dc1d44e --- /dev/null +++ b/libs/basic_games/games/game_trainsimulator.py @@ -0,0 +1,36 @@ +from PyQt6.QtCore import QFileInfo + +import mobase + +from ..basic_game import BasicGame + + +class RailworksGame(BasicGame): + Name = "Train Simulator Classic Support Plugin" + Author = "Ryan Young" + Version = "1.1.0" + + GameName = "Train Simulator" + GameShortName = "railworks" + GameBinary = "RailWorks.exe" + GameDataPath = "" + GameSteamId = "24010" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Train-Simulator-Classic" + ) + + def executables(self): + game_directory = self.gameDirectory() + executables: list[tuple[str, str]] = [ + ("32-bit", "RailWorks.exe"), + ("64-bit", "RailWorks64.exe"), + ("64-bit, DirectX 12", "RailWorksDX12_64.exe"), + ] + return [ + mobase.ExecutableInfo( + f"Train Simulator ({name})", + QFileInfo(game_directory.absoluteFilePath(path)), + ) + for name, path in executables + ] diff --git a/libs/basic_games/games/game_valheim.py b/libs/basic_games/games/game_valheim.py new file mode 100644 index 0000000..c402323 --- /dev/null +++ b/libs/basic_games/games/game_valheim.py @@ -0,0 +1,427 @@ +from __future__ import annotations + +import itertools +import re +import shutil +from collections.abc import Collection, Iterable, Mapping, Sequence +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Optional, TextIO + +from PyQt6.QtCore import QDir + +import mobase + +from ..basic_features import BasicLocalSavegames, BasicModDataChecker, GlobPatterns +from ..basic_features.basic_save_game_info import BasicGameSaveGame +from ..basic_game import BasicGame + + +def move_file(source: Path, target: Path): + """Move `source` to `target`. Creates missing (parent) directories and + overwrites existing `target`.""" + if not target.parent.exists(): + target.parent.mkdir(parents=True) + shutil.move(str(source.resolve()), str(target.resolve())) + + +@dataclass +class PartialMatch: + partial_match_regex: re.Pattern[str] = re.compile(r"[A-Z]?[a-z]+") + """Matches words, for e.g. 'Camel' and 'Case' in 'CamelCase'.""" + + exclude: set[str] = field(default_factory=set[str]) + min_length: int = 3 + + def partial_match(self, str_with_parts: str, search_string: str) -> Collection[str]: + """Returns partial matches of the first string (`str_with_parts`) + in the `search_string`. + + See: + `partial_match_regex` + """ + parts = self.partial_match_regex.finditer(str_with_parts) + search_string_lower = search_string.casefold() + return set( + p_lower + for p in parts + if len(p_lower := p[0].casefold()) >= self.min_length + and p_lower not in self.exclude + and p_lower in search_string_lower + ) + + +@dataclass +class ContentMatch: + file_glob_patterns: Iterable[str] + """File patterns (glob) for content files and `content_regex`.""" + content_regex: re.Pattern[str] + """Regex to get mod name from content. with (?P<`match_group`>) group.""" + match_group: str + + def match_content(self, file_path: Path) -> str: + if ( + self.content_regex + and self.file_glob_patterns + and any(file_path.match(p) for p in self.file_glob_patterns) + ): + with open(file_path) as file: + if match := self.content_regex.search(file.read()): + return match.group(self.match_group) + return "" + + +class DebugTable: + """Debug in markdown table.""" + + def __init__(self, column_keys: Collection[str]) -> None: + """Init the table, adds the header. + + Args: + column_keys: The column keys for the table. + """ + self.new_table(column_keys) + + def __call__(self, **kwargs: Any) -> None: + self.add(**kwargs) + + def new_table(self, column_keys: Collection[str] | None = None) -> None: + if column_keys: + self._column_keys = column_keys + self._table: list[dict[str, str]] = [ + {c: c for c in self._column_keys}, + {c: "-" * len(c) for c in self._column_keys}, + ] + + def add( + self, + **kwargs: Any, + ) -> None: + """Add data to the table. Adds a new line if the last row has already data in + for the column key. + + Args: + **kwargs: the values per column key for a row. + """ + for k, v in kwargs.items(): + if not self._table or self._table[-1].get(k, ""): + # Append line if element in last list is set. + self._table.append(dict.fromkeys(self._column_keys, "")) + self._table[-1][k] = str(v) + + def print(self, output_file: Optional[TextIO] = None): + if self._table: + for line in self._table: + print("|", " | ".join(line.values()), "|", file=output_file) + if output_file: + output_file.flush() + self._table = [] + + +class OverwriteSync: + organizer: mobase.IOrganizer + game: mobase.IPluginGame + search_file_contents: bool = True + + overwrite_file_pattern: Iterable[str] = ["BepInEx/config/*"] + """File pattern (glob) in overwrite folder.""" + partial_match: PartialMatch = PartialMatch(exclude={"valheim", "mod"}) + content_match: ContentMatch = ContentMatch( + file_glob_patterns=["*.cfg"], + content_regex=re.compile(r"\A.*plugin (?P.+) v[\d.]+?$", re.I | re.M), + match_group="mod", + ) + + _debug = DebugTable("overwrite_file | mod | target_path | matches".split(" | ")) + + def __init__(self, organizer: mobase.IOrganizer, game: mobase.IPluginGame) -> None: + self.organizer = organizer + self.game = game + + def sync(self) -> None: + """Sync the Overwrite folder (back) to the mods.""" + print("Syncing Overwrite with mods") + modlist = self.organizer.modList() + mod_map = self._get_active_mods(modlist) + mod_dll_map = self._get_mod_dll_map(mod_map) + overwrite_path = Path(self.organizer.overwritePath()) + self._debug.new_table() + for pattern in self.overwrite_file_pattern: + for file_path in overwrite_path.glob(pattern): + self._debug(overwrite_file=file_path.name) + if mod := self._find_mod_for_overwrite_file(file_path, mod_dll_map): + # Move cfg to mod folder + mod_path = Path(modlist.getMod(mod).absolutePath()) + target_path = mod_path / file_path.relative_to(overwrite_path) + self._debug(mod=mod, target_path=target_path) + move_file(file_path, target_path) + self._debug.print() + + def _get_active_mods( + self, modlist: mobase.IModList | None = None + ) -> dict[str, mobase.IModInterface]: + """Get all active mods. + + Args: modlist (optional): the `mobase.IModList`. Defaults to None (get it from + `self._organizer`). + + Returns: `{mod_name: mobase.IModInterface}` + """ + modlist = modlist or self.organizer.modList() + + return { + name: mod + for name in modlist.allMods() # allModsByProfilePriority ? + if (mod := modlist.getMod(name)).gameName() == self.game.gameShortName() + and not mod.isForeign() + and not mod.isBackup() + and not mod.isSeparator() + and (modlist.state(name) & mobase.ModState.ACTIVE) + } + + def _get_mod_dll_map(self, mod_map: Mapping[str, str | mobase.IModInterface]): + return {name: self._get_mod_dlls(mod) for name, mod in mod_map.items()} + + def _get_mod_dlls(self, mod: str | mobase.IModInterface) -> Sequence[str]: + """Get all BepInEx/plugins/*.dll files of a mod.""" + if isinstance(mod, str): + mod = self.organizer.modList().getMod(mod) + plugins = mod.fileTree().find("BepInEx/plugins/", mobase.IFileTree.DIRECTORY) + if isinstance(plugins, mobase.IFileTree): + return [name for p in plugins if (name := p.name()).endswith(".dll")] + else: + return [] + + def _find_mod_for_overwrite_file( + self, + file_path: Path, + mod_dll_map: Mapping[str, Collection[str]], + ) -> str: + """Find the mod (name) matching a file in Overwrite (using the mods dll name). + + Args: + file_path: The name of the file. + mod_dll_map: Mods names and their dll files `{mod_name: ["ModName.dll"]}`. + + Returns: + The name of the mod matching the given file_name best. + If there is no clear mod match for the file, an empty string "" is returned. + """ + if file_path.is_dir(): + return "" + file_name = file_path.stem + # matching metric: combined length of partial matches per mod. + matching_mods = self._get_matching_mods(file_name, mod_dll_map) + if len(matching_mods) == 0: + if self.search_file_contents and self.content_match: + # Get mod name from file content. + long_mod_name = self.content_match.match_content(file_path) + matching_mods = self._get_matching_mods(long_mod_name, mod_dll_map) + if len(matching_mods) == 1: + # Only a single mod match found. + return matching_mods[0][0] + elif len(matching_mods) > 1: + # Find mod with longest (combined) partial_match. + self._debug.add(matches=matching_mods) + # Do not return a mod for multiple "equal" matches. + if matching_mods[0][1] > matching_mods[1][1]: + return matching_mods[0][0] + return "" + + def _get_matching_mods( + self, search_str: str, mod_dll_map: Mapping[str, Collection[str]] + ) -> Sequence[tuple[str, int, set[str]]]: + """Find matching mods for the given `search_str`. + + Args: + search_str: A string to find a mod match for. + mod_dll_map: Mods names and their dll files `{mod_name: ["ModName.dll"]}`. + + Returns: + Mods with partial matches, sorted descending by their metric + (length of combined partial matches): + {mod_name: (len_of_combined_partial_matches, {partial_matches, ...}), ...} + """ + return sorted( + ( + (name, sum(len(s) for s in partial_matches), partial_matches) + for name, dlls in mod_dll_map.items() + if len( + partial_matches := set( + itertools.chain.from_iterable( + self.partial_match.partial_match(search_str, dll) + for dll in dlls + ) + ) + ) + > 0 + ), + key=lambda x: x[1], + reverse=True, + ) + + +class ValheimSaveGame(BasicGameSaveGame): + def getName(self) -> str: + return f"[{self.getSaveGroupIdentifier().rstrip('s')}] {self._filepath.stem}" + + def getSaveGroupIdentifier(self) -> str: + return self._filepath.parent.name + + def allFiles(self) -> list[str]: + files = super().allFiles() + files.extend( + self._filepath.with_suffix(suffix).as_posix() + for suffix in [self._filepath.suffix + ".old"] + ) + return files + + +class ValheimWorldSaveGame(ValheimSaveGame): + def allFiles(self) -> list[str]: + files = super().allFiles() + files.extend( + self._filepath.with_suffix(suffix).as_posix() + for suffix in [".db", ".db.old"] + ) + return files + + +class ValheimGame(BasicGame): + Name = "Valheim Support Plugin" + Author = "Zash" + Version = "1.3" + + GameName = "Valheim" + GameShortName = "valheim" + GameThunderstoreName = "valheim" + GameNexusId = 3667 + GameSteamId = [892970, 896660, 1223920] + GameBinary = "valheim.exe" + GameDataPath = "" + GameSavesDirectory = r"%USERPROFILE%/AppData/LocalLow/IronGate/Valheim" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/Game:-Valheim" + ) + + _forced_libraries = ["winhttp.dll"] + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature( + BasicModDataChecker( + GlobPatterns( + unfold=[ + "BepInExPack_Valheim", + ], + valid=[ + "meta.ini", # Included in installed mod folder. + "BepInEx", + "doorstop_libs", + "unstripped_corlib", + ".doorstop_version", + "doorstop_config.ini", + "start_game_bepinex.sh", + "start_server_bepinex.sh", + "winhttp.dll", + "changelog.txt", + # + "InSlimVML", + "valheim_Data", + "inslimvml.ini", + # + "unstripped_managed", + # + "AdvancedBuilder", + ], + ignore=[ + "*.mohidden", + ], + delete=[ + "*.txt", + "*.md", + "README", + "icon.png", + "license", + "manifest.json", + "*.dll.mdb", + "*.pdb", + ], + move={ + "*_VML.dll": "InSlimVML/Mods/", + # + "plugins": "BepInEx/", + "Jotunn": "BepInEx/plugins/", + "*.dll": "BepInEx/plugins/", + "*.xml": "BepInEx/plugins/", + "Translations": "BepInEx/plugins/", + "config": "BepInEx/", + "*.cfg": "BepInEx/config/", + # + "CustomTextures": "BepInEx/plugins/", + "*.png": "BepInEx/plugins/CustomTextures/", + # + "Builds": "AdvancedBuilder/", + "*.vbuild": "AdvancedBuilder/Builds/", + # + "*.assets": "valheim_Data/", + }, + ) + ) + ) + self._register_feature(BasicLocalSavegames(self)) + self._overwrite_sync = OverwriteSync(organizer=self._organizer, game=self) + self._register_event_handler() + return True + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + return [ + mobase.ExecutableForcedLoadSetting(self.binaryName(), lib).withEnabled(True) + for lib in self._forced_libraries + ] + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + save_games = super().listSaves(folder) + path = Path(folder.absolutePath()) + save_games.extend(ValheimSaveGame(f) for f in path.glob("characters/*.fch")) + save_games.extend(ValheimWorldSaveGame(f) for f in path.glob("worlds/*.fwl")) + return save_games + + def settings(self) -> list[mobase.PluginSetting]: + settings = super().settings() + settings.extend( + [ + mobase.PluginSetting( + "sync_overwrite", "Sync overwrite with mods", True + ), + mobase.PluginSetting( + "search_overwrite_file_content", + "Search content of files in overwrite for matching mod", + True, + ), + ] + ) + return settings + + def _register_event_handler(self): + self._organizer.onUserInterfaceInitialized(lambda win: self._sync_overwrite()) + self._organizer.onFinishedRun(self._game_finished_event_handler) + + def _game_finished_event_handler(self, app_path: str, exit_code: int) -> None: + """Sync overwrite folder with mods after game was closed.""" + if Path(app_path) == Path( + self.gameDirectory().absolutePath(), self.binaryName() + ): + self._sync_overwrite() + + def _sync_overwrite(self) -> None: + if self._organizer.managedGame() is not self: + return + if self._organizer.pluginSetting(self.name(), "sync_overwrite") is not False: + self._overwrite_sync.search_file_contents = ( + self._organizer.pluginSetting( + self.name(), "search_overwrite_file_content" + ) + is not False + ) + self._overwrite_sync.sync() diff --git a/libs/basic_games/games/game_valkyriachronicles.py b/libs/basic_games/games/game_valkyriachronicles.py new file mode 100644 index 0000000..d208de6 --- /dev/null +++ b/libs/basic_games/games/game_valkyriachronicles.py @@ -0,0 +1,15 @@ +from ..basic_game import BasicGame + + +class ValkyriaChroniclesGame(BasicGame): + Name = "Valkyria Chronicles Support Plugin" + Author = "Ketsuban" + Version = "1.0.0" + + GameName = "Valkyria Chronicles" + GameShortName = "vc1" + GameBinary = "Valkyria.exe" + GameLauncher = "Launcher.exe" + GameDataPath = "%GAME_PATH%" + GameSavesDirectory = "%GAME_PATH%/savedata" + GameSteamId = 294860 diff --git a/libs/basic_games/games/game_vampirebloodlines.py b/libs/basic_games/games/game_vampirebloodlines.py new file mode 100644 index 0000000..a63b308 --- /dev/null +++ b/libs/basic_games/games/game_vampirebloodlines.py @@ -0,0 +1,103 @@ +from pathlib import Path +from typing import List + +from PyQt6.QtCore import QDir + +import mobase + +from ..basic_features import BasicLocalSavegames +from ..basic_game import BasicGame, BasicGameSaveGame + + +class VampireModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + self.validDirNames = [ + "cfg", + "cl_dlls", + "dlg", + "dlls", + "maps", + "materials", + "media", + "models", + "particles", + "python", + "resource", + "scripts", + "sound", + "vdata", + ] + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + for entry in filetree: + if not entry.isDir(): + continue + if entry.name().casefold() in self.validDirNames: + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.INVALID + + +class VampireSaveGame(BasicGameSaveGame): + _filepath: Path + + def __init__(self, filepath: Path): + super().__init__(filepath) + self._filepath = filepath + self.name = None + self.elapsedTime = None + + +class VampireTheMasqueradeBloodlinesGame(BasicGame): + Name = "Vampire - The Masquerade: Bloodlines Support Plugin" + Author = "John" + Version = "1.0.0" + Description = "Adds support for Vampires: The Masquerade - Bloodlines" + + GameName = "Vampire - The Masquerade: Bloodlines" + GameShortName = "vampirebloodlines" + GameNexusName = "vampirebloodlines" + GameNexusId = 437 + GameSteamId = [2600] + GameGogId = [1207659240] + GameBinary = "vampire.exe" + GameDataPath = "vampire" + GameDocumentsDirectory = "%GAME_PATH%/vampire/cfg" + GameSavesDirectory = "%GAME_PATH%/vampire/SAVE" + GameSaveExtension = "sav" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Vampire:-The-Masquerade-%E2%80%90-Bloodlines" + ) + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(VampireModDataChecker()) + self._register_feature(BasicLocalSavegames(self)) + return True + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + # Create .cfg files if they don't exist + for iniFile in self.iniFiles(): + iniPath = Path(self.documentsDirectory().absoluteFilePath(iniFile)) + if not iniPath.exists(): + with open(iniPath, "w") as _: + pass + + super().initializeProfile(directory, settings) + + def version(self): + # Don't forget to import mobase! + return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.FINAL) + + def iniFiles(self): + return ["autoexec.cfg", "user.cfg"] + + def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + VampireSaveGame(path) + for path in Path(folder.absolutePath()).glob(f"*.{ext}") + ] diff --git a/libs/basic_games/games/game_witcher1.py b/libs/basic_games/games/game_witcher1.py new file mode 100644 index 0000000..4d76b58 --- /dev/null +++ b/libs/basic_games/games/game_witcher1.py @@ -0,0 +1,93 @@ +from pathlib import Path +from typing import BinaryIO, List + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_game import BasicGame, BasicGameSaveGame + + +class Witcher1SaveGame(BasicGameSaveGame): + def __init__(self, filepath: Path): + super().__init__(filepath) + self.areaName: str = "" + self.parseSaveFile(filepath) + + @staticmethod + def readInt(fp: BinaryIO, length: int = 4) -> int: + return int.from_bytes(fp.read(length), "little") + + @staticmethod + def readFixedString(fp: BinaryIO, length: int) -> str: + b: bytes = fp.read(length) + res = b.decode("utf-16") + return res.rstrip("\0") + + def parseSaveFile(self, filepath: Path): + # https://github.com/xoreos/xoreos/blob/82bd991052732ab1f8f75f512b3dfabfcc92ae8f/src/aurora/thewitchersavefile.cpp#L60 + with filepath.open(mode="rb") as fp: + magic = fp.read(4) + if magic != b"RGMH": + raise ValueError("Invalid TheWitcherSave file!") + + version = self.readInt(fp) + if version != 1: + raise ValueError("Invalid TheWitcherSave file!") + + # TODO: get the preview image + # dataOffset = self.readInt(fp, 8) + fp.seek(8, 1) + fp.seek(8, 1) + fp.seek(4 * 4, 1) + + lightningStorm = self.readFixedString(fp, 2048) + if lightningStorm != "Lightning Storm": + raise ValueError('Missing "Lightning Storm"') + + areaName1 = self.readFixedString(fp, 2048) + areaName2 = self.readFixedString(fp, 2048) + + if areaName1 != areaName2: + raise ValueError("Invalid Area Name!") + + self.areaName = areaName1 + + def getName(self) -> str: + return self.areaName + + +class Witcher1Game(BasicGame): + Name = "Witcher 1 Support Plugin" + Author = "erri120" + Version = "1.0.0" + + GameName = "The Witcher: Enhanced Edition" + GameShortName = "witcher" + GameNexusName = "witcher" + GameNexusId = 150 + GameSteamId = 20900 + GameGogId = 1207658924 + GameBinary = "System/witcher.exe" + GameDataPath = "Data" + GameSaveExtension = "TheWitcherSave" + GameDocumentsDirectory = "%DOCUMENTS%/The Witcher" + GameSavesDirectory = "%GAME_DOCUMENTS%/saves" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-The-Witcher" + ) + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + return True + + def executables(self) -> List[mobase.ExecutableInfo]: + path = QFileInfo(self.gameDirectory(), "System/witcher.exe") + return [mobase.ExecutableInfo("The Witcher", path)] + + def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: + return [ + Witcher1SaveGame(path) + for path in Path(folder.absolutePath()).glob("*.TheWitcherSave") + ] diff --git a/libs/basic_games/games/game_witcher2.py b/libs/basic_games/games/game_witcher2.py new file mode 100644 index 0000000..d38bb6b --- /dev/null +++ b/libs/basic_games/games/game_witcher2.py @@ -0,0 +1,67 @@ +from pathlib import Path +from typing import List + +from PyQt6.QtCore import QDir + +import mobase + +from ..basic_features import BasicGameSaveGameInfo +from ..basic_features.basic_save_game_info import BasicGameSaveGame +from ..basic_game import BasicGame + + +class Witcher2SaveGame(BasicGameSaveGame): + def allFiles(self): + return [ + self._filepath.name, + self._filepath.name.replace(".sav", "_640x360.bmp"), + ] + + +class Witcher2Game(BasicGame): + Name = "Witcher 2 Support Plugin" + Author = "DefinitelyNotSade" + Version = "1.0.0" + + GameName = "The Witcher 2: Assassins of Kings" + GameShortName = "witcher2" + GaneNexusHame = "witcher2" + # GameNexusId = 952 + GameSteamId = 20920 + GameGogId = 1207658930 + GameLauncher = "Launcher.exe" + GameBinary = "bin/witcher2.exe" + GameDataPath = "CookedPC" + GameSaveExtension = "sav" + GameDocumentsDirectory = "%DOCUMENTS%/witcher 2/Config" + GameSavesDirectory = "%GAME_DOCUMENTS%/../gamesaves" + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._register_feature( + BasicGameSaveGameInfo( + lambda s: Path(str(s.parent) + "\\" + s.stem + "_640x360").with_suffix( + ".bmp" + ) + ) + ) + return True + + def iniFiles(self): + return [ + "User.ini", + "Rendering.ini", + "Community.ini", + "UserContent.ini", + "DIMapping.ini", + "Input_QWERTY.ini", + "Input_AZERTY.ini", + "Input_QWERTZ.ini", + ] + + def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + Witcher2SaveGame(path) + for path in Path(folder.absolutePath()).glob(f"*.{ext}") + ] diff --git a/libs/basic_games/games/game_witcher3.py b/libs/basic_games/games/game_witcher3.py new file mode 100644 index 0000000..ff64df2 --- /dev/null +++ b/libs/basic_games/games/game_witcher3.py @@ -0,0 +1,52 @@ +from pathlib import Path +from typing import List + +from PyQt6.QtCore import QDir + +import mobase + +from ..basic_features import BasicGameSaveGameInfo +from ..basic_features.basic_save_game_info import BasicGameSaveGame +from ..basic_game import BasicGame + + +class Witcher3SaveGame(BasicGameSaveGame): + def allFiles(self): + return [self._filepath.name, self._filepath.name.replace(".sav", ".png")] + + +class Witcher3Game(BasicGame): + Name = "Witcher 3 Support Plugin" + Author = "Holt59" + Version = "1.0.0a" + + GameName = "The Witcher 3: Wild Hunt" + GameShortName = "witcher3" + GaneNexusHame = "witcher3" + GameNexusId = 952 + GameSteamId = [499450, 292030] + GameGogId = [1640424747, 1495134320, 1207664663, 1207664643] + GameBinary = "bin/x64/witcher3.exe" + GameDataPath = "Mods" + GameSaveExtension = "sav" + GameDocumentsDirectory = "%DOCUMENTS%/The Witcher 3" + GameSavesDirectory = "%GAME_DOCUMENTS%/gamesaves" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-The-Witcher-3" + ) + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._register_feature(BasicGameSaveGameInfo(lambda s: s.with_suffix(".png"))) + return True + + def iniFiles(self): + return ["user.settings", "input.settings"] + + def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + Witcher3SaveGame(path) + for path in Path(folder.absolutePath()).glob(f"*.{ext}") + ] diff --git a/libs/basic_games/games/game_x4.py b/libs/basic_games/games/game_x4.py new file mode 100644 index 0000000..a1a7041 --- /dev/null +++ b/libs/basic_games/games/game_x4.py @@ -0,0 +1,17 @@ +from ..basic_game import BasicGame + + +class X4FoundationsGame(BasicGame): + Name = "X4 Foundations Support Plugin" + Author = "Twinki,BrandonM4" + Version = "0.1.0" + + GameName = "X4: Foundations" + GameShortName = "x4foundations" + + GameDocumentsDirectory = "%DOCUMENTS%/Egosoft/X4" + GameBinary = "x4.exe" + GameDataPath = "extensions" + + GameNexusId = 2659 + GameSteamId = 392160 diff --git a/libs/basic_games/games/game_xplane11.py b/libs/basic_games/games/game_xplane11.py new file mode 100644 index 0000000..ecbe89a --- /dev/null +++ b/libs/basic_games/games/game_xplane11.py @@ -0,0 +1,16 @@ +from ..basic_game import BasicGame + + +class XP11Game(BasicGame): + Name = "X-Plane 11 Support Plugin" + Author = "Deorder" + Version = "0.0.1" + + GameName = "X-Plane 11" + GameShortName = "xp11" + GameBinary = r"X-Plane.exe" + GameDataPath = r"" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-X%E2%80%90Plane-11" + ) diff --git a/libs/basic_games/games/game_zeusandposeidon.py b/libs/basic_games/games/game_zeusandposeidon.py new file mode 100644 index 0000000..33a8c5a --- /dev/null +++ b/libs/basic_games/games/game_zeusandposeidon.py @@ -0,0 +1,70 @@ +from typing import List, Optional + +import mobase + +from ..basic_game import BasicGame + + +class ZeusAndPoseidonModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + folders: List[mobase.IFileTree] = [] + files: List[mobase.FileTreeEntry] = [] + + for entry in filetree: + if isinstance(entry, mobase.IFileTree): + folders.append(entry) + else: + files.append(entry) + + if len(folders) != 1: + return mobase.ModDataChecker.INVALID + + folder = folders[0] + pakfile = folder.name() + ".pak" + if folder.exists(pakfile): + if filetree.exists(pakfile): + return mobase.ModDataChecker.VALID + else: + return mobase.ModDataChecker.FIXABLE + + return mobase.ModDataChecker.INVALID + + def fix(self, filetree: mobase.IFileTree) -> Optional[mobase.IFileTree]: + first_entry = filetree[0] + if not isinstance(first_entry, mobase.IFileTree): + return None + entry = first_entry.find(filetree[0].name() + ".pak") + if entry is None: + return None + filetree.copy(entry, "", mobase.IFileTree.InsertPolicy.FAIL_IF_EXISTS) + return filetree + + +class ZeusAndPoseidonGame(BasicGame): + Name = "Zeus and Poseidon Support Plugin" + Author = "Holt59" + Version = "1.0.0a" + + GameName = "Zeus and Poseidon" + GameShortName = "zeusandposeidon" # No Nexus support + GameSteamId = 566050 + GameGogId = 1207659039 + GameBinary = "Zeus.exe" + GameDataPath = "Adventures" + GameDocumentsDirectory = "%GAME_PATH%" + GameSavesDirectory = "%GAME_PATH%/Save" + GameSaveExtension = "sav" + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Zeus%EF%BC%8BPoseidon" + ) + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._register_feature(ZeusAndPoseidonModDataChecker()) + return True diff --git a/libs/basic_games/games/oblivion_remaster/__init__.py b/libs/basic_games/games/oblivion_remaster/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/basic_games/games/oblivion_remaster/constants.py b/libs/basic_games/games/oblivion_remaster/constants.py new file mode 100644 index 0000000..656e296 --- /dev/null +++ b/libs/basic_games/games/oblivion_remaster/constants.py @@ -0,0 +1,14 @@ +from typing import TypedDict + +PLUGIN_NAME = "Oblivion Remastered Support Plugin" + + +class UE4SSModInfo(TypedDict): + mod_name: str + mod_enabled: bool + + +DEFAULT_UE4SS_MODS: list[UE4SSModInfo] = [ + {"mod_name": "BPML_GenericFunctions", "mod_enabled": True}, + {"mod_name": "BPModLoaderMod", "mod_enabled": True}, +] diff --git a/libs/basic_games/games/oblivion_remaster/game_plugins.py b/libs/basic_games/games/oblivion_remaster/game_plugins.py new file mode 100644 index 0000000..6777da6 --- /dev/null +++ b/libs/basic_games/games/oblivion_remaster/game_plugins.py @@ -0,0 +1,207 @@ +from functools import cmp_to_key +from typing import Sequence + +from PyQt6.QtCore import ( + QByteArray, + QCoreApplication, + QDateTime, + QFile, + QFileInfo, + QStringConverter, + QStringEncoder, + qCritical, + qWarning, +) + +import mobase + + +class OblivionRemasteredGamePlugins(mobase.GamePlugins): + """ + Reimplementation of GameGamebryo "GamePlugins" code, in the Skyrim style. + + Should properly account for disabled plugins and the loadorder.txt profile file. + """ + + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self._last_read = QDateTime().currentDateTime() + self._organizer = organizer + # Not currently used. These plugins exist in the base game but are not enabled by default. + self._plugin_blacklist = ["TamrielLevelledRegion.esp", "AltarGymNavigation.esp"] + + def writePluginLists(self, plugin_list: mobase.IPluginList) -> None: + if not self._last_read.isValid(): + return + self.writePluginList( + plugin_list, self._organizer.profile().absolutePath() + "/plugins.txt" + ) + self.writeLoadOrderList( + plugin_list, self._organizer.profile().absolutePath() + "/loadorder.txt" + ) + self._last_read = QDateTime.currentDateTime() + + def readPluginLists(self, plugin_list: mobase.IPluginList) -> None: + load_order_path = self._organizer.profile().absolutePath() + "/loadorder.txt" + load_order = self.readLoadOrderList(plugin_list, load_order_path) + plugin_list.setLoadOrder(load_order) + self.readPluginList(plugin_list) + self._last_read = QDateTime.currentDateTime() + + def getLoadOrder(self) -> Sequence[str]: + load_order_path = self._organizer.profile().absolutePath() + "/loadorder.txt" + plugins_path = self._organizer.profile().absolutePath() + "/plugins.txt" + + load_order_is_new = ( + not self._last_read.isValid() + or not QFileInfo(load_order_path).exists() + or QFileInfo(load_order_path).lastModified() > self._last_read + ) + plugins_is_new = ( + not self._last_read.isValid() + or QFileInfo(plugins_path).lastModified() > self._last_read + ) + + if load_order_is_new or not plugins_is_new: + return self.readLoadOrderList(self._organizer.pluginList(), load_order_path) + else: + return self.readPluginList(self._organizer.pluginList()) + + def writePluginList(self, plugin_list: mobase.IPluginList, filePath: str): + self.writeList(plugin_list, filePath, False) + + def writeLoadOrderList(self, plugin_list: mobase.IPluginList, filePath: str): + self.writeList(plugin_list, filePath, True) + + def writeList( + self, plugin_list: mobase.IPluginList, filePath: str, load_order: bool + ): + plugins_file = open(filePath, "w") + encoder = ( + QStringEncoder(QStringConverter.Encoding.Utf8) + if load_order + else QStringEncoder(QStringConverter.Encoding.System) + ) + plugins_text = "# This file was automatically generated by Mod Organizer.\n" + invalid_filenames = False + written_count = 0 + plugins = plugin_list.pluginNames() + plugins_sorted = sorted( + plugins, + key=cmp_to_key( + lambda lhs, rhs: plugin_list.priority(lhs) - plugin_list.priority(rhs) + ), + ) + for plugin_name in plugins_sorted: + if ( + load_order + or plugin_list.state(plugin_name) == mobase.PluginState.ACTIVE + ): + result = encoder.encode(plugin_name) + if encoder.hasError(): + invalid_filenames = True + qCritical("invalid plugin name %s" % plugin_name) + plugins_text += result.data().decode() + "\n" + written_count += 1 + + if invalid_filenames: + qCritical( + QCoreApplication.translate( + "MainWindow", + "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 written_count == 0: + qWarning( + "plugin list would be empty, this is almost certainly wrong. Not saving." + ) + else: + plugins_file.write(plugins_text) + plugins_file.close() + + def readLoadOrderList( + self, plugin_list: mobase.IPluginList, file_path: str + ) -> list[str]: + plugin_names = [ + plugin for plugin in self._organizer.managedGame().primaryPlugins() + ] + plugin_lookup: set[str] = set() + for name in plugin_names: + if name.lower() not in plugin_lookup: + plugin_lookup.add(name.lower()) + + try: + with open(file_path) as file: + for line in file: + if line.startswith("#"): + continue + plugin_file = line.rstrip("\n") + if plugin_file.lower() not in plugin_lookup: + plugin_lookup.add(plugin_file.lower()) + plugin_names.append(plugin_file) + except FileNotFoundError: + return self.readPluginList(plugin_list) + + return plugin_names + + def readPluginList(self, plugin_list: mobase.IPluginList) -> list[str]: + plugins = [plugin for plugin in plugin_list.pluginNames()] + sorted_plugins: list[str] = [] + primary = [plugin for plugin in self._organizer.managedGame().primaryPlugins()] + primary_lower = [plugin.lower() for plugin in primary] + for plugin_name in primary: + if plugin_list.state(plugin_name) != mobase.PluginState.MISSING: + plugin_list.setState(plugin_name, mobase.PluginState.ACTIVE) + sorted_plugins.append(plugin_name) + plugin_remove = [ + plugin for plugin in plugins if plugin.lower() in primary_lower + ] + for plugin in plugin_remove: + plugins.remove(plugin) + + plugins_txt_exists = True + file_path = self._organizer.profile().absolutePath() + "/plugins.txt" + file = QFile(file_path) + if not file.open(QFile.OpenModeFlag.ReadOnly): + plugins_txt_exists = False + if file.size() == 0: + plugins_txt_exists = False + if plugins_txt_exists: + while not file.atEnd(): + line = file.readLine() + file_plugin_name = QByteArray() + if line.size() > 0 and line.at(0).decode() != "#": + encoder = QStringEncoder(QStringEncoder.Encoding.System) + file_plugin_name = encoder.encode(line.trimmed().data().decode()) + if file_plugin_name.size() > 0: + if file_plugin_name.data().decode().lower() in [ + plugin.lower() for plugin in plugins + ]: + plugin_list.setState( + file_plugin_name.data().decode(), mobase.PluginState.ACTIVE + ) + sorted_plugins.append(file_plugin_name.data().decode()) + plugins.remove(file_plugin_name.data().decode()) + + file.close() + + for plugin_name in plugins: + plugin_list.setState(plugin_name, mobase.PluginState.INACTIVE) + else: + for plugin_name in plugins: + plugin_list.setState(plugin_name, mobase.PluginState.INACTIVE) + + return sorted_plugins + plugins + + def lightPluginsAreSupported(self) -> bool: + return False + + def mediumPluginsAreSupported(self) -> bool: + return False + + def blueprintPluginsAreSupported(self) -> bool: + return False diff --git a/libs/basic_games/games/oblivion_remaster/mod_data_checker.py b/libs/basic_games/games/oblivion_remaster/mod_data_checker.py new file mode 100644 index 0000000..51875b2 --- /dev/null +++ b/libs/basic_games/games/oblivion_remaster/mod_data_checker.py @@ -0,0 +1,451 @@ +from typing import cast + +import mobase + +from .constants import PLUGIN_NAME + + +def _parent(entry: mobase.FileTreeEntry): + """ + Same as entry.parent() but always returns a mobase.IFileTree and never None. + """ + return cast(mobase.IFileTree, entry.parent()) + + +class OblivionRemasteredModDataChecker(mobase.ModDataChecker): + # These directories are generally considered valid, but may require additional checks. + # These represent top level directories in the mod. + _dirs = ["Data", "Paks", "OBSE", "Movies", "UE4SS", "GameSettings", "Root"] + # Directories considered valid for 'Data' though many of these may be obsolete. + _data_dirs = [ + "meshes", + "textures", + "music", + "fonts", + "interface", + "shaders", + "strings", + "materials", + ] + # Data file extensions considered valid. Unclear if BSAs are actually used. + _data_extensions = [".esm", ".esp", ".bsa"] + + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self._organizer = organizer + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + status = mobase.ModDataChecker.INVALID + # These represent common mod structures that include UE4SS base files. + # These should generally be pruned or moved into a Root Builder path. + if filetree.find("ue4ss/UE4SS.dll") is not None: + return mobase.ModDataChecker.FIXABLE + elif ( + filetree.find("OblivionRemastered/Binaries/Win64/ue4ss/UE4SS.dll") + is not None + ): + return mobase.ModDataChecker.FIXABLE + + # Crawl the directory tree to check mod structure. + for entry in filetree: + name = entry.name().casefold() + parent = entry.parent() + assert parent is not None + # These are top-level file entries. + if parent.parent() is None: + if isinstance(entry, mobase.IFileTree): + # Look for valid top level directories. + if name in [dirname.lower() for dirname in self._dirs]: + if name == "ue4ss": + """ + The UE4SS mod directory should contain either mod directories with + a 'scripts/main.lua' file, or 'shared' library files. Certain common + 'preset settings' files are also acceptable. + """ + mods = entry.find("Mods") + if isinstance(mods, mobase.IFileTree): + """ + UE4SS intrinsically maps to the 'Mods' directory, so if this directory + is present, it should be relocated. + """ + for sub_entry in mods: + if isinstance(sub_entry, mobase.IFileTree): + if sub_entry.find("scripts/main.lua"): + status = mobase.ModDataChecker.FIXABLE + break + if sub_entry.name().casefold() in [ + "shared", + "npcappearancemanager", + "naturalbodymorph", + ]: + status = mobase.ModDataChecker.FIXABLE + break + else: + for sub_entry in entry: + if isinstance(sub_entry, mobase.IFileTree): + # Files are present in the correct directory. Mark valid. + if sub_entry.find("scripts/main.lua"): + status = mobase.ModDataChecker.VALID + break + if sub_entry.name().casefold() in [ + "shared", + "npcappearancemanager", + "naturalbodymorph", + ]: + status = mobase.ModDataChecker.VALID + break + else: + # All other base directories are considered valid + status = mobase.ModDataChecker.VALID + # No need to continue checks if the directory looks valid + if status == mobase.ModDataChecker.VALID: + break + elif name in [dirname.lower() for dirname in self._data_dirs]: + # Found a 'Data' subdirectory. Should be moved into 'Data'. + status = mobase.ModDataChecker.FIXABLE + else: + # Parse other directories for potential mod files. + for sub_entry in entry: + if sub_entry.isFile(): + sub_name = sub_entry.name().casefold() + if sub_name.endswith(".exe"): + # Trying to handle EXE files is problematic, let the user figure it out + return mobase.ModDataChecker.INVALID + if sub_name.endswith((".pak", ".bk2")): + # Found Pak files or movie files, should be fixable + status = mobase.ModDataChecker.FIXABLE + elif sub_name.endswith(tuple(self._data_extensions)): + # Found plugins or BSA files, should be fixable + status = mobase.ModDataChecker.FIXABLE + else: + if name == "Paks": + # Found Paks directory as subdirectory, should be fixable + status = mobase.ModDataChecker.FIXABLE + # Iterate into subdirectories so we can check the entire archive + new_status = self.dataLooksValid(entry) + if new_status != mobase.ModDataChecker.INVALID: + status = new_status + if status == mobase.ModDataChecker.VALID: + break + else: + if name.endswith(".exe"): + return mobase.ModDataChecker.INVALID + if name.endswith(tuple(self._data_extensions + [".pak", ".bk2"])): + status = mobase.ModDataChecker.FIXABLE + else: + # Section is for parsing subdirectories + if isinstance(entry, mobase.IFileTree): + if name in [dir_name.lower() for dir_name in self._dirs]: + status = mobase.ModDataChecker.FIXABLE + if name in [dir_name.lower() for dir_name in self._data_dirs]: + status = mobase.ModDataChecker.FIXABLE + else: + new_status = self.dataLooksValid(entry) + if new_status != mobase.ModDataChecker.INVALID: + status = new_status + else: + if name.endswith(".exe"): + return mobase.ModDataChecker.INVALID + if name.endswith( + tuple(self._data_extensions + [".pak", ".lua", ".bk2"]) + ): + status = mobase.ModDataChecker.FIXABLE + if status == mobase.ModDataChecker.VALID: + break + return status + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + """ + Main fixer function. Iterates files, using 'parse_directory' for subdirectory search and fixing. + """ + + # UE4SS is packaged with many mods (or standalone) and should be processed into Root if found. + # This can avoid a lot of unnecessary iterations. + ue4ss_dll = filetree.find("ue4ss/UE4SS.dll") + if ue4ss_dll is None: + ue4ss_dll = filetree.find( + "OblivionRemastered/Binaries/Win64/ue4ss/UE4SS.dll" + ) + if ue4ss_dll is not None and (ue4ss_folder := ue4ss_dll.parent()) is not None: + entries: list[mobase.FileTreeEntry] = [] + for entry in _parent(ue4ss_folder): + entries.append(entry) + for entry in entries: + filetree.move( + entry, + "Root/OblivionRemastered/Binaries/Win64/", + mobase.IFileTree.MERGE, + ) + + # Similar to the above, many mods pack files relative to the root game directory. Some common paths can be + # automatically moved into the appropriate directory structures to avoid needless iteration. + exe_dir = filetree.find(r"OblivionRemastered\Binaries\Win64") + if isinstance(exe_dir, mobase.IFileTree): + gamesettings_dir = exe_dir.find("GameSettings") + if isinstance(gamesettings_dir, mobase.IFileTree): + gamesettings_main = self.get_dir(filetree, "GameSettings") + gamesettings_main.merge(gamesettings_dir, True) + self.detach_parents(gamesettings_dir) + obse_dir = exe_dir.find("OBSE") + if isinstance(obse_dir, mobase.IFileTree): + obse_main = self.get_dir(filetree, "OBSE") + obse_main.merge(obse_dir, True) + self.detach_parents(obse_dir) + ue4ss_mod_dir = exe_dir.find("ue4ss/Mods") + if isinstance(ue4ss_mod_dir, mobase.IFileTree): + if self._organizer.pluginSetting(PLUGIN_NAME, "ue4ss_use_root_builder"): + ue4ss_main = self.get_dir( + filetree, "Root/OblivionRemastered/Binaries/Win64/ue4ss/Mods" + ) + else: + ue4ss_main = self.get_dir(filetree, "UE4SS") + ue4ss_main.merge(ue4ss_mod_dir, True) + self.detach_parents(ue4ss_mod_dir) + if len(exe_dir): + root_exe_dir = self.get_dir( + filetree, "Root/OblivionRemastered/Binaries" + ) + parent = exe_dir.parent() + exe_dir.moveTo(root_exe_dir) + if parent: + self.detach_parents(parent) + else: + self.detach_parents(exe_dir) + + # Start the main directory iteration code + directories: list[mobase.IFileTree] = [] + for entry in filetree: + if isinstance(entry, mobase.IFileTree): + directories.append(entry) + for directory in directories: + if directory.name().casefold() in [ + dirname.lower() for dirname in self._data_dirs + ]: + # Move detected 'Data' directories into 'Data' + data_dir = self.get_dir(filetree, "Data") + directory.moveTo(data_dir) + elif directory.name().casefold() == "ue4ss": + # Validate and correct UE4SS mod files into 'UE4SS' + # Alternately, can use Root Builder path per user request + mods = directory.find("Mods") + if isinstance(mods, mobase.IFileTree): + for sub_entry in mods: + if isinstance(sub_entry, mobase.IFileTree): + if ( + sub_entry.find("scripts/main.lua") + or sub_entry.name().casefold() == "shared" + ): + if self._organizer.pluginSetting( + PLUGIN_NAME, "ue4ss_use_root_builder" + ): + ue4ss_main = self.get_dir( + filetree, + "Root/OblivionRemastered/Binaries/Win64/ue4ss/Mods", + ) + sub_entry.moveTo(ue4ss_main) + self.detach_parents(directory) + else: + parent = _parent(sub_entry) + sub_entry.moveTo(directory) + self.detach_parents(parent) + elif directory.name().casefold() not in [ + dirname.lower() for dirname in self._dirs + ]: + # For non-valid directories, iterate into the directory + filetree = self.parse_directory(filetree, directory) + # Parsing top-level files + entries: list[mobase.FileTreeEntry] = [] + # As we are changing the filetree, cache the current directory list and iterate on that + for entry in filetree: + entries.append(entry) + for entry in entries: + if entry.parent() == filetree and entry.isFile(): + name = entry.name().casefold() + if name.endswith(".pak"): + # Move all pak|ucas|utoc files into "Paks\~mods" + paks_dir = self.get_dir(filetree, "Paks/~mods") + pak_files: list[mobase.FileTreeEntry] = [] + for file in _parent(entry): + if file.isFile(): + if ( + file.name() + .casefold() + .endswith((".pak", ".ucas", ".utoc")) + ): + pak_files.append(file) + for pak_file in pak_files: + pak_file.moveTo(paks_dir) + elif name.endswith(".bk2"): + # Top-level bk2 files should be moved to "Movies\Modern" + movies_dir = self.get_dir(filetree, "Movies/Modern") + movie_files: list[mobase.FileTreeEntry] = [] + for file in _parent(entry): + if file.isFile(): + if file.name().casefold().endswith(".bk2"): + movie_files.append(file) + for movie_file in movie_files: + movie_file.moveTo(movies_dir) + elif name.endswith(tuple(self._data_extensions)): + # Files matching Data file extensions should be moved to "Data" + data_dir = self.get_dir(filetree, "Data") + data_files: list[mobase.FileTreeEntry] = [] + for file in _parent(entry): + data_files.append(file) + for data_file in data_files: + data_file.moveTo(data_dir) + return filetree + + def parse_directory( + self, main_filetree: mobase.IFileTree, next_dir: mobase.IFileTree + ) -> mobase.IFileTree: + """ + Subdirectory iterator for fix(). Allows parsing all subdirectory files. + + :param main_filetree: The main mod filetree that should be returned. + :param next_dir: The currently processed subdirectory of main_filetree. + :returns: The updated main_filetree. + """ + directories: list[mobase.IFileTree] = [] + for entry in next_dir: + if isinstance(entry, mobase.IFileTree): + directories.append(entry) + for directory in directories: + name = directory.name().casefold() + stop = False + for dir_name in self._dirs: + if name == dir_name.lower(): + main_dir = self.get_dir(main_filetree, dir_name) + if name == "ue4ss": + # UE4SS directories should presumably map to 'UE4SS' but check for a 'Mods' directory and move that instead. + if self._organizer.pluginSetting( + PLUGIN_NAME, "ue4ss_use_root_builder" + ): + ue4ss_dir = self.get_dir( + main_filetree, + "Root/OblivionRemastered/Binaries/Win64/ue4ss", + ) + ue4ss_dir.merge(directory) + else: + mod_dir = directory.find("Mods") + if isinstance(mod_dir, mobase.IFileTree): + main_dir.merge(mod_dir) + else: + main_dir.merge(directory) + else: + main_dir.merge(directory) + self.detach_parents(directory) + stop = True + break + if stop: + continue + if name in ["~mods", "logicmods"]: + # These directories should represent Paks mods and should be moved into that directory. + paks_dir = self.get_dir(main_filetree, "Paks") + directory.moveTo(paks_dir) + continue + elif name in [dirname.lower() for dirname in self._data_dirs]: + # These directories are typically associated with Data and should be moved into that directory. + data_dir = self.get_dir(main_filetree, "Data") + data_dir.merge(directory) + self.detach_parents(directory) + continue + main_filetree = self.parse_directory(main_filetree, directory) + for entry in next_dir: + if entry.isFile(): + name = entry.name().casefold() + if name.endswith(tuple(self._data_extensions)): + # Files matching Data extensions should be moved into 'Data' + data_dir = self.get_dir(main_filetree, "Data") + data_dir.merge(next_dir) + self.detach_parents(next_dir) + elif name.endswith(".pak"): + # Loose pak files most likely should be installed to 'Paks\~mods' but check the parent directory + paks_dir = self.get_dir(main_filetree, "Paks") + if next_dir.name().casefold() == "paks": + paks_dir.merge(next_dir) + self.detach_parents(next_dir) + return main_filetree + elif next_dir.name().casefold() in ["~mods", "logicmods"]: + next_dir.moveTo(paks_dir) + return main_filetree + else: + parent = _parent(next_dir) + main_filetree.move( + next_dir, "Paks/~mods/", mobase.IFileTree.MERGE + ) + + self.detach_parents(parent) + return main_filetree + elif name.endswith(".lua"): + # LUA files are generally associated with UE4SS mods. Most will probably be found before this point. + if next_dir.parent() and next_dir.parent() != main_filetree: + parent = _parent(next_dir).parent() + if self._organizer.pluginSetting( + PLUGIN_NAME, "ue4ss_use_root_builder" + ): + ue4ss_main = self.get_dir( + main_filetree, + "Root/OblivionRemastered/Binaries/Win64/ue4ss/Mods", + ) + _parent(next_dir).moveTo(ue4ss_main) + else: + if main_filetree.find("UE4SS") is None: + main_filetree.addDirectory("UE4SS") + main_filetree.move( + _parent(next_dir), + "UE4SS/", + mobase.IFileTree.MERGE, + ) + if parent is not None: + self.detach_parents(parent) + return main_filetree + elif name.endswith(".bk2"): + movies_dir = self.get_dir(main_filetree, "Movies/Modern") + movies_dir.merge(next_dir) + self.detach_parents(next_dir) + + return main_filetree + + def detach_parents(self, directory: mobase.IFileTree) -> None: + """ + Attempts to clean up empty archive directories after files have been moved. + Find the top-most directory of an empty file tree where only the child directory is contained. + Remove that filetree. + + :param directory: The directory tree to be pruned (starting with the lowest element). + """ + + if ( + directory.parent() is not None + and (parent := directory.parent()) is not None + and len(parent) == 1 + ): + parent = parent if parent.parent() is not None else directory + while ( + parent + and (p_parent := parent.parent()) is not None + and (pp_parent := p_parent.parent()) is not None + and len(pp_parent) == 1 + ): + parent = parent.parent() + + assert parent is not None + parent.detach() + else: + directory.detach() + + def get_dir(self, filetree: mobase.IFileTree, directory: str) -> mobase.IFileTree: + """ + Simple helper function that finds or creates a directory within a given filetree. + + :param filetree: The filetree in which to file or create the given directory + :param directory: The directory name to return + :return: The filetree of the created or found directory. + """ + + tree_dir = filetree.find(directory) + if not isinstance(tree_dir, mobase.IFileTree): + tree_dir = filetree.addDirectory(directory) + return tree_dir diff --git a/libs/basic_games/games/oblivion_remaster/mod_data_content.py b/libs/basic_games/games/oblivion_remaster/mod_data_content.py new file mode 100644 index 0000000..2ee7e5f --- /dev/null +++ b/libs/basic_games/games/oblivion_remaster/mod_data_content.py @@ -0,0 +1,109 @@ +from enum import IntEnum, auto + +import mobase + + +class Content(IntEnum): + PLUGIN = auto() + BSA = auto() + PAK = auto() + OBSE = auto() + OBSE_FILES = auto() + MOVIE = auto() + UE4SS = auto() + MAGIC_LOADER = auto() + GAME_SETTINGS = auto() + + +class OblivionRemasteredDataContent(mobase.ModDataContent): + OR_CONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.PLUGIN, "Plugins (ESM/ESP)", ":/MO/gui/content/plugin"), + (Content.BSA, "Bethesda Archive", ":/MO/gui/content/bsa"), + (Content.PAK, "Paks", ":/MO/gui/content/geometries"), + (Content.OBSE, "Script Extender Plugin", ":/MO/gui/content/skse"), + (Content.OBSE_FILES, "Script Extender Files", "", True), + (Content.MOVIE, "Movies", ":/MO/gui/content/media"), + (Content.UE4SS, "UE4SS Mods", ":/MO/gui/content/script"), + (Content.MAGIC_LOADER, "Magic Loader Mod", ":/MO/gui/content/inifile"), + (Content.GAME_SETTINGS, "Game Settings", ":/MO/gui/content/menu"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.OR_CONTENTS + ] + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + contents: set[int] = set() + + for entry in filetree: + if isinstance(entry, mobase.IFileTree): + match entry.name().casefold(): + case "data": + for data_entry in entry: + if data_entry.isFile(): + match data_entry.suffix().casefold(): + case "esm" | "esp": + contents.add(Content.PLUGIN) + case "bsa": + contents.add(Content.BSA) + case _: + pass + else: + match data_entry.name().casefold(): + case "magicloader": + contents.add(Content.MAGIC_LOADER) + case _: + pass + case "obse": + contents.add(Content.OBSE_FILES) + plugins_dir = entry.find("Plugins") + if isinstance(plugins_dir, mobase.IFileTree): + for plugin_entry in plugins_dir: + if ( + plugin_entry.isFile() + and plugin_entry.suffix().casefold() == "dll" + ): + contents.add(Content.OBSE) + if ( + isinstance(plugin_entry, mobase.IFileTree) + and plugins_dir.name().casefold() == "gamesettings" + ): + for settings_file in plugin_entry: + if ( + settings_file.isFile() + and settings_file.suffix().casefold() + == "ini" + ): + contents.add(Content.GAME_SETTINGS) + case "paks": + contents.add(Content.PAK) + for paks_entry in entry: + if isinstance(paks_entry, mobase.IFileTree): + if paks_entry.name().casefold() == "~mods": + for mods_entry in paks_entry: + if isinstance(mods_entry, mobase.IFileTree): + if ( + "magicloader" + in mods_entry.name().casefold() + ): + contents.add(Content.MAGIC_LOADER) + break + if paks_entry.name().casefold() == "logicmods": + contents.add(Content.UE4SS) + case "movies": + contents.add(Content.MOVIE) + case "ue4ss": + contents.add(Content.UE4SS) + case "gamesettings": + for settings_file in entry: + if ( + settings_file.isFile() + and settings_file.suffix().casefold() == "ini" + ): + contents.add(Content.GAME_SETTINGS) + case _: + pass + + return list(contents) diff --git a/libs/basic_games/games/oblivion_remaster/paks/__init__.py b/libs/basic_games/games/oblivion_remaster/paks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/basic_games/games/oblivion_remaster/paks/model.py b/libs/basic_games/games/oblivion_remaster/paks/model.py new file mode 100644 index 0000000..c43cec0 --- /dev/null +++ b/libs/basic_games/games/oblivion_remaster/paks/model.py @@ -0,0 +1,253 @@ +import itertools +import typing +from enum import IntEnum, auto +from typing import Any, TypeAlias, overload + +from PyQt6.QtCore import ( + QAbstractItemModel, + QByteArray, + QDataStream, + QDir, + QFileInfo, + QMimeData, + QModelIndex, + QObject, + Qt, + QVariant, +) +from PyQt6.QtWidgets import QWidget + +import mobase + +_PakInfo: TypeAlias = tuple[str, str, str, str] + + +class PaksColumns(IntEnum): + PRIORITY = auto() + PAK_NAME = auto() + SOURCE = auto() + + +class PaksModel(QAbstractItemModel): + def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): + super().__init__(parent) + self.paks: dict[int, _PakInfo] = {} + self._organizer = organizer + self._init_mod_states() + + def _init_mod_states(self): + profile = QDir(self._organizer.profilePath()) + paks_txt = QFileInfo(profile.absoluteFilePath("paks.txt")) + if paks_txt.exists(): + with open(paks_txt.absoluteFilePath(), "r") as paks_file: + index = 0 + for line in paks_file: + self.paks[index] = (line, "", "", "") + index += 1 + + def set_paks(self, paks: dict[int, _PakInfo]): + self.layoutAboutToBeChanged.emit() + self.paks = paks + self.layoutChanged.emit() + self.dataChanged.emit( + self.index(0, 0), + self.index(self.rowCount(), self.columnCount()), + [Qt.ItemDataRole.DisplayRole], + ) + + def flags(self, index: QModelIndex) -> Qt.ItemFlag: + if not index.isValid(): + return ( + Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsDragEnabled + | Qt.ItemFlag.ItemIsDropEnabled + | Qt.ItemFlag.ItemIsEnabled + ) + return ( + super().flags(index) + | Qt.ItemFlag.ItemIsDragEnabled + | Qt.ItemFlag.ItemIsDropEnabled & Qt.ItemFlag.ItemIsEditable + ) + + def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: + return len(PaksColumns) + + def index( + self, row: int, column: int, parent: QModelIndex = QModelIndex() + ) -> QModelIndex: + if ( + row < 0 + or row >= self.rowCount() + or column < 0 + or column >= self.columnCount() + ): + return QModelIndex() + return self.createIndex(row, column, row) + + @overload + def parent(self, child: QModelIndex) -> QModelIndex: ... + @overload + def parent(self) -> QObject | None: ... + + def parent(self, child: QModelIndex | None = None) -> QModelIndex | QObject | None: + if child is None: + return super().parent() + return QModelIndex() + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + return len(self.paks) + + def setData( + self, index: QModelIndex, value: Any, role: int = Qt.ItemDataRole.EditRole + ) -> bool: + return False + + def headerData( + self, + section: int, + orientation: Qt.Orientation, + role: int = Qt.ItemDataRole.DisplayRole, + ) -> typing.Any: + if ( + orientation != Qt.Orientation.Horizontal + or role != Qt.ItemDataRole.DisplayRole + ): + return QVariant() + + column = PaksColumns(section + 1) + match column: + case PaksColumns.PAK_NAME: + return "Pak Group" + case PaksColumns.PRIORITY: + return "Priority" + case PaksColumns.SOURCE: + return "Source" + + return QVariant() + + def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: + if not index.isValid(): + return None + if index.column() + 1 == PaksColumns.PAK_NAME: + if role == Qt.ItemDataRole.DisplayRole: + return self.paks[index.row()][0] + elif index.column() + 1 == PaksColumns.PRIORITY: + if role == Qt.ItemDataRole.DisplayRole: + return index.row() + elif index.column() + 1 == PaksColumns.SOURCE: + if role == Qt.ItemDataRole.DisplayRole: + return self.paks[index.row()][1] + return QVariant() + + def canDropMimeData( + self, + data: QMimeData | None, + action: Qt.DropAction, + row: int, + column: int, + parent: QModelIndex, + ) -> bool: + if action == Qt.DropAction.MoveAction and (row != -1 or column != -1): + return True + return False + + def supportedDropActions(self) -> Qt.DropAction: + return Qt.DropAction.MoveAction + + def dropMimeData( + self, + data: QMimeData | None, + action: Qt.DropAction, + row: int, + column: int, + parent: QModelIndex, + ) -> bool: + if action == Qt.DropAction.IgnoreAction: + return True + + if data is None: + return False + + encoded: QByteArray = data.data("application/x-qabstractitemmodeldatalist") + stream: QDataStream = QDataStream(encoded, QDataStream.OpenModeFlag.ReadOnly) + source_rows: list[int] = [] + + while not stream.atEnd(): + source_row = stream.readInt() + col = stream.readInt() + size = stream.readInt() + item_data = {} + for _ in range(size): + role = stream.readInt() + value = stream.readQVariant() + item_data[role] = value + if col == 0: + source_rows.append(source_row) + + if row == -1: + row = parent.row() + + if row < 0 or row >= len(self.paks): + new_priority = len(self.paks) + else: + new_priority = row + + before_paks: list[_PakInfo] = [] + moved_paks: list[_PakInfo] = [] + after_paks: list[_PakInfo] = [] + before_paks_p: list[_PakInfo] = [] + moved_paks_p: list[_PakInfo] = [] + after_paks_p: list[_PakInfo] = [] + for row, paks in sorted(self.paks.items()): + if row < new_priority: + if row in source_rows: + if paks[0].casefold()[-2:] == "_p": + moved_paks_p.append(paks) + else: + moved_paks.append(paks) + else: + if paks[0].casefold()[-2:] == "_p": + before_paks_p.append(paks) + else: + before_paks.append(paks) + if row >= new_priority: + if row in source_rows: + if paks[0].casefold()[-2:] == "_p": + moved_paks_p.append(paks) + else: + moved_paks.append(paks) + else: + if paks[0].casefold()[-2:] == "_p": + after_paks_p.append(paks) + else: + after_paks.append(paks) + + new_paks = dict( + enumerate( + itertools.chain( + before_paks, + moved_paks, + after_paks, + before_paks_p, + moved_paks_p, + after_paks_p, + ) + ) + ) + + index = 8999 + for row, pak in new_paks.items(): + current_dir = QDir(pak[2]) + parent_dir = QDir(pak[2]) + parent_dir.cdUp() + if current_dir.exists() and parent_dir.dirName().casefold() == "~mods": + new_paks[row] = ( + pak[0], + pak[1], + pak[2], + parent_dir.absoluteFilePath(str(index).zfill(4)), + ) + index -= 1 + + self.set_paks(new_paks) + return False diff --git a/libs/basic_games/games/oblivion_remaster/paks/view.py b/libs/basic_games/games/oblivion_remaster/paks/view.py new file mode 100644 index 0000000..a56b0cd --- /dev/null +++ b/libs/basic_games/games/oblivion_remaster/paks/view.py @@ -0,0 +1,33 @@ +from typing import Iterable + +from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal +from PyQt6.QtGui import QDropEvent +from PyQt6.QtWidgets import QAbstractItemView, QTreeView, QWidget + + +class PaksView(QTreeView): + data_dropped = pyqtSignal() + + def __init__(self, parent: QWidget | None): + super().__init__(parent) + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.setDropIndicatorShown(True) + self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + self.setDefaultDropAction(Qt.DropAction.MoveAction) + if (viewport := self.viewport()) is not None: + viewport.setAcceptDrops(True) + self.setItemsExpandable(False) + self.setRootIsDecorated(False) + + def dropEvent(self, e: QDropEvent | None): + super().dropEvent(e) + self.clearSelection() + self.data_dropped.emit() + + def dataChanged( + self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () + ): + super().dataChanged(topLeft, bottomRight, roles) + self.repaint() diff --git a/libs/basic_games/games/oblivion_remaster/paks/widget.py b/libs/basic_games/games/oblivion_remaster/paks/widget.py new file mode 100644 index 0000000..efc3038 --- /dev/null +++ b/libs/basic_games/games/oblivion_remaster/paks/widget.py @@ -0,0 +1,213 @@ +from functools import cmp_to_key +from pathlib import Path +from typing import cast + +from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QGridLayout, QWidget + +import mobase + +from ....basic_features.utils import is_directory +from .model import PaksModel +from .view import PaksView + + +def pak_sort(a: tuple[str, str], b: tuple[str, str]) -> int: + a_pak, a_str = a[0], a[1] or a[0] + b_pak, b_str = b[0], b[1] or b[0] + + a_pak_ends_p = a_pak.casefold().endswith("_p") + b_pak_ends_p = b_pak.casefold().endswith("_p") + + if a_pak_ends_p == b_pak_ends_p: + if a_str.casefold() <= b_str.casefold(): + return 1 + return -1 + elif a_pak_ends_p: + return 1 + elif b_pak_ends_p: + return -1 + return 0 + + +class PaksTabWidget(QWidget): + def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): + super().__init__(parent) + self._organizer = organizer + self._view = PaksView(self) + self._layout = QGridLayout(self) + self._layout.addWidget(self._view) + self._model = PaksModel(self._view, organizer) + self._view.setModel(self._model) + self._model.dataChanged.connect(self.write_paks_list) # type: ignore + self._view.data_dropped.connect(self.write_paks_list) # type: ignore + organizer.onProfileChanged(lambda profile_a, profile_b: self._parse_pak_files()) + organizer.modList().onModInstalled(lambda mod: self._parse_pak_files()) + organizer.modList().onModRemoved(lambda mod: self._parse_pak_files()) + organizer.modList().onModStateChanged(lambda mods: self._parse_pak_files()) + self._parse_pak_files() + + def load_paks_list(self) -> list[str]: + profile = QDir(self._organizer.profilePath()) + paks_txt = QFileInfo(profile.absoluteFilePath("paks.txt")) + paks_list: list[str] = [] + if paks_txt.exists(): + with open(paks_txt.absoluteFilePath(), "r") as paks_file: + for line in paks_file: + paks_list.append(line.strip()) + return paks_list + + def write_paks_list(self): + profile = QDir(self._organizer.profilePath()) + paks_txt = QFileInfo(profile.absoluteFilePath("paks.txt")) + with open(paks_txt.absoluteFilePath(), "w") as paks_file: + for _, pak in sorted(self._model.paks.items()): + name, _, _, _ = pak + paks_file.write(f"{name}\n") + self.write_pak_files() + + def write_pak_files(self): + for index, pak in sorted(self._model.paks.items()): + _, _, current_path, target_path = pak + if current_path and current_path != target_path: + path_dir = Path(current_path) + target_dir = Path(target_path) + if not target_dir.exists(): + target_dir.mkdir(parents=True, exist_ok=True) + if path_dir.exists(): + for pak_file in path_dir.glob("*.pak"): + ucas_file = pak_file.with_suffix(".ucas") + utoc_file = pak_file.with_suffix(".utoc") + for file in (pak_file, ucas_file, utoc_file): + if not file.exists(): + continue + try: + file.rename(target_dir.joinpath(file.name)) + except FileExistsError: + pass + data = self._model.paks[index] + self._model.paks[index] = ( + data[0], + data[1], + data[3], + data[3], + ) + break + if not list(path_dir.iterdir()): + path_dir.rmdir() + + def _shake_paks(self, sorted_paks: dict[str, str]) -> list[str]: + shaken_paks: list[str] = [] + shaken_paks_p: list[str] = [] + paks_list = self.load_paks_list() + for pak in paks_list: + if pak in sorted_paks.keys(): + if pak.casefold().endswith("_p"): + shaken_paks_p.append(pak) + else: + shaken_paks.append(pak) + sorted_paks.pop(pak) + for pak in sorted_paks.keys(): + if pak.casefold().endswith("_p"): + shaken_paks_p.append(pak) + else: + shaken_paks.append(pak) + return shaken_paks + shaken_paks_p + + def _parse_pak_files(self): + from ...game_oblivion_remaster import OblivionRemasteredGame + + mods = self._organizer.modList().allMods() + paks: dict[str, str] = {} + pak_paths: dict[str, tuple[str, str]] = {} + pak_source: dict[str, str] = {} + for mod in mods: + mod_item = self._organizer.modList().getMod(mod) + if not self._organizer.modList().state(mod) & mobase.ModState.ACTIVE: + continue + filetree = mod_item.fileTree() + pak_mods = filetree.find("Paks/~mods") + if not pak_mods: + pak_mods = filetree.find("Root/OblivionRemastered/Content/Paks/~mods") + if isinstance(pak_mods, mobase.IFileTree): + for entry in pak_mods: + if is_directory(entry): + if "magicloader" in entry.name().casefold(): + continue + for sub_entry in entry: + if ( + sub_entry.isFile() + and sub_entry.suffix().casefold() == "pak" + ): + pak_name = sub_entry.name()[ + : -1 - len(sub_entry.suffix()) + ] + paks[pak_name] = entry.name() + pak_paths[pak_name] = ( + mod_item.absolutePath() + + "/" + + cast(mobase.IFileTree, sub_entry.parent()).path( + "/" + ), + mod_item.absolutePath() + "/" + pak_mods.path("/"), + ) + pak_source[pak_name] = mod_item.name() + else: + if entry.suffix().casefold() == "pak": + pak_name = entry.name()[: -1 - len(entry.suffix())] + paks[pak_name] = "" + pak_paths[pak_name] = ( + mod_item.absolutePath() + + "/" + + cast(mobase.IFileTree, entry.parent()).path("/"), + mod_item.absolutePath() + "/" + pak_mods.path("/"), + ) + pak_source[pak_name] = mod_item.name() + game = self._organizer.managedGame() + if isinstance(game, OblivionRemasteredGame): + pak_mods = QFileInfo(game.paksDirectory().absoluteFilePath("~mods")) + if pak_mods.exists() and pak_mods.isDir(): + for entry in QDir(pak_mods.absoluteFilePath()).entryInfoList( + QDir.Filter.Dirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot + ): + if entry.isDir(): + if "magicloader" in entry.completeBaseName().casefold(): + continue + for sub_entry in QDir(entry.absoluteFilePath()).entryInfoList( + QDir.Filter.Files + ): + if ( + sub_entry.isFile() + and sub_entry.suffix().casefold() == "pak" + ): + pak_name = sub_entry.completeBaseName() + paks[pak_name] = entry.completeBaseName() + pak_paths[pak_name] = ( + sub_entry.absolutePath(), + pak_mods.absolutePath(), + ) + pak_source[pak_name] = "Game Directory" + else: + if entry.suffix().casefold() == "pak": + pak_name = entry.completeBaseName() + paks[pak_name] = "" + pak_paths[pak_name] = ( + entry.absolutePath(), + pak_mods.absolutePath(), + ) + pak_source[pak_name] = "Game Directory" + sorted_paks = dict(sorted(paks.items(), key=cmp_to_key(pak_sort))) + shaken_paks: list[str] = self._shake_paks(sorted_paks) + final_paks: dict[str, tuple[str, str, str]] = {} + pak_index = 8999 + for pak in shaken_paks: + target_dir = pak_paths[pak][1] + "/" + str(pak_index).zfill(4) + final_paks[pak] = (pak_source[pak], pak_paths[pak][0], target_dir) + pak_index -= 1 + new_data_paks: dict[int, tuple[str, str, str, str]] = {} + i = 0 + for pak, data in final_paks.items(): + source, current_path, target_path = data + new_data_paks[i] = (pak, source, current_path, target_path) + i += 1 + self._model.set_paks(new_data_paks) diff --git a/libs/basic_games/games/oblivion_remaster/script_extender.py b/libs/basic_games/games/oblivion_remaster/script_extender.py new file mode 100644 index 0000000..571e675 --- /dev/null +++ b/libs/basic_games/games/oblivion_remaster/script_extender.py @@ -0,0 +1,37 @@ +from pathlib import Path + +import mobase + + +class OblivionRemasteredScriptExtender(mobase.ScriptExtender): + def __init__(self, game: mobase.IPluginGame): + super().__init__() + self._game = game + + def binaryName(self): + return "obse64_loader.exe" + + def loaderName(self) -> str: + return self.binaryName() + + def loaderPath(self) -> str: + return ( + self._game.gameDirectory().absolutePath() + + "\\OblivionRemastered\\Binaries\\Win64\\" + + self.loaderName() + ) + + def pluginPath(self) -> str: + return "OBSE/Plugins" + + def savegameExtension(self) -> str: + return "" + + def isInstalled(self) -> bool: + return Path(self.loaderPath()).exists() + + def getExtenderVersion(self) -> str: + return mobase.getFileVersion(self.loaderPath()) + + def getArch(self) -> int: + return 0x8664 if self.isInstalled() else 0x0 diff --git a/libs/basic_games/games/oblivion_remaster/ue4ss/__init__.py b/libs/basic_games/games/oblivion_remaster/ue4ss/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/basic_games/games/oblivion_remaster/ue4ss/model.py b/libs/basic_games/games/oblivion_remaster/ue4ss/model.py new file mode 100644 index 0000000..a3804fe --- /dev/null +++ b/libs/basic_games/games/oblivion_remaster/ue4ss/model.py @@ -0,0 +1,125 @@ +import json +from json import JSONDecodeError +from typing import Any, Iterable + +from PyQt6.QtCore import ( + QDir, + QFileInfo, + QMimeData, + QModelIndex, + QStringListModel, + Qt, +) +from PyQt6.QtWidgets import QWidget + +import mobase + +from ..constants import DEFAULT_UE4SS_MODS + + +class UE4SSListModel(QStringListModel): + def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): + super().__init__(parent) + self._checked_items: set[str] = set() + self._organizer = organizer + self._init_mod_states() + + def _init_mod_states(self): + profile = QDir(self._organizer.profilePath()) + mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if mods_json.exists(): + with open(mods_json.absoluteFilePath(), "r") as json_file: + try: + mod_data = json.load(json_file) + except JSONDecodeError: + mod_data = DEFAULT_UE4SS_MODS + for mod in mod_data: + if mod["mod_enabled"]: + self._checked_items.add(mod["mod_name"]) + + def _set_mod_states(self): + profile = QDir(self._organizer.profilePath()) + mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + mod_list: dict[str, bool] = {} + if mods_json.exists(): + with open(mods_json.absoluteFilePath(), "r") as json_file: + try: + mod_data = json.load(json_file) + except JSONDecodeError: + mod_data = DEFAULT_UE4SS_MODS + for mod in mod_data: + mod_list[mod["mod_name"]] = mod["mod_enabled"] + for i in range(self.rowCount()): + item = self.index(i, 0) + name = self.data(item, Qt.ItemDataRole.DisplayRole) + if name in mod_list: + self.setData( + item, + True if mod_list[name] else False, + Qt.ItemDataRole.CheckStateRole, + ) + else: + self.setData(item, True, Qt.ItemDataRole.CheckStateRole) + + def flags(self, index: QModelIndex) -> Qt.ItemFlag: + flags = super().flags(index) + if not index.isValid(): + return ( + Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsDragEnabled + | Qt.ItemFlag.ItemIsDropEnabled + | Qt.ItemFlag.ItemIsEnabled + ) + return ( + flags + | Qt.ItemFlag.ItemIsUserCheckable + | Qt.ItemFlag.ItemIsDragEnabled & Qt.ItemFlag.ItemIsEditable + ) + + def setData( + self, index: QModelIndex, value: Any, role: int = Qt.ItemDataRole.EditRole + ) -> bool: + if not index.isValid() or role != Qt.ItemDataRole.CheckStateRole: + return False + + if ( + bool(value) + and self.data(index, Qt.ItemDataRole.DisplayRole) not in self._checked_items + ): + self._checked_items.add(self.data(index, Qt.ItemDataRole.DisplayRole)) + elif ( + not bool(value) + and self.data(index, Qt.ItemDataRole.DisplayRole) in self._checked_items + ): + self._checked_items.remove(self.data(index, Qt.ItemDataRole.DisplayRole)) + self.dataChanged.emit(index, index, [role]) + return True + + def setStringList(self, strings: Iterable[str | None]): + super().setStringList(strings) + self._set_mod_states() + + def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: + if not index.isValid(): + return None + + if role == Qt.ItemDataRole.CheckStateRole: + return ( + Qt.CheckState.Checked + if self.data(index, Qt.ItemDataRole.DisplayRole) in self._checked_items + else Qt.CheckState.Unchecked + ) + + return super().data(index, role) + + def canDropMimeData( + self, + data: QMimeData | None, + action: Qt.DropAction, + row: int, + column: int, + parent: QModelIndex, + ) -> bool: + if action == Qt.DropAction.MoveAction and (row != -1 or column != -1): + return True + return False diff --git a/libs/basic_games/games/oblivion_remaster/ue4ss/view.py b/libs/basic_games/games/oblivion_remaster/ue4ss/view.py new file mode 100644 index 0000000..bb994ca --- /dev/null +++ b/libs/basic_games/games/oblivion_remaster/ue4ss/view.py @@ -0,0 +1,31 @@ +from typing import Iterable + +from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal +from PyQt6.QtGui import QDropEvent +from PyQt6.QtWidgets import QAbstractItemView, QListView, QWidget + + +class UE4SSView(QListView): + data_dropped = pyqtSignal() + + def __init__(self, parent: QWidget | None): + super().__init__(parent) + self.setAcceptDrops(True) + self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.setDropIndicatorShown(True) + self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + self.setDefaultDropAction(Qt.DropAction.MoveAction) + if (viewport := self.viewport()) is not None: + viewport.setAcceptDrops(True) + + def dropEvent(self, e: QDropEvent | None): + super().dropEvent(e) + self.data_dropped.emit() + + def dataChanged( + self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () + ): + super().dataChanged(topLeft, bottomRight, roles) + self.repaint() diff --git a/libs/basic_games/games/oblivion_remaster/ue4ss/widget.py b/libs/basic_games/games/oblivion_remaster/ue4ss/widget.py new file mode 100644 index 0000000..246b7fa --- /dev/null +++ b/libs/basic_games/games/oblivion_remaster/ue4ss/widget.py @@ -0,0 +1,175 @@ +import json +from functools import cmp_to_key +from json import JSONDecodeError +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo, Qt +from PyQt6.QtWidgets import QGridLayout, QWidget + +import mobase + +from ..constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .model import UE4SSListModel +from .view import UE4SSView + + +class UE4SSTabWidget(QWidget): + def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): + super().__init__(parent) + self._organizer = organizer + self._view = UE4SSView(self) + self._layout = QGridLayout(self) + self._layout.addWidget(self._view) + self._model = UE4SSListModel(self._view, organizer) + self._view.setModel(self._model) + self._model.dataChanged.connect(self.write_mod_list) # type: ignore + self._view.data_dropped.connect(self.write_mod_list) # type: ignore + organizer.onProfileChanged(lambda profile_a, profile_b: self._parse_mod_files()) + organizer.modList().onModInstalled(self.update_mod_files) + organizer.modList().onModRemoved(lambda mod: self._parse_mod_files()) + organizer.modList().onModStateChanged(self.update_mod_files) + self._parse_mod_files() + + def get_mod_list(self) -> list[str]: + mod_list: list[str] = [] + for index in range(self._model.rowCount()): + mod_list.append( + self._model.data( + self._model.index(index, 0), Qt.ItemDataRole.DisplayRole + ) + ) + return mod_list + + def update_mod_files( + self, mods: dict[str, mobase.ModState] | mobase.IModInterface | str + ): + mod_list: list[mobase.IModInterface] = [] + if isinstance(mods, dict): + for mod in mods.keys(): + mod_list.append(self._organizer.modList().getMod(mod)) + elif isinstance(mods, mobase.IModInterface): + mod_list.append(mods) + else: + mod_list.append(self._organizer.modList().getMod(mods)) + + for mod in mod_list: + tree = mod.fileTree() + ue4ss_files = tree.find("UE4SS") + if not ue4ss_files: + ue4ss_files = tree.find( + "Root/OblivionRemastered/Binaries/Win64/ue4ss/Mods" + ) + if isinstance(ue4ss_files, mobase.IFileTree): + for entry in ue4ss_files: + if isinstance(entry, mobase.IFileTree): + if enabled_txt := entry.find("enabled.txt"): + try: + Path(mod.absolutePath(), enabled_txt.path("/")).unlink() + self._organizer.modDataChanged(mod) + except FileNotFoundError: + pass + + self._parse_mod_files() + + def _parse_mod_files(self): + from ...game_oblivion_remaster import OblivionRemasteredGame + + mod_list: set[str] = set() + for mod in self._organizer.modList().allMods(): + if ( + mobase.ModState(self._organizer.modList().state(mod)) + & mobase.ModState.ACTIVE + ): + tree = self._organizer.modList().getMod(mod).fileTree() + ue4ss_files = tree.find("UE4SS") + if not ue4ss_files: + ue4ss_files = tree.find( + "Root/OblivionRemastered/Binaries/Win64/ue4ss/Mods" + ) + if isinstance(ue4ss_files, mobase.IFileTree): + for entry in ue4ss_files: + if isinstance(entry, mobase.IFileTree): + if entry.find("scripts/main.lua"): + mod_list.add(entry.name()) + if enabled_txt := entry.find("enabled.txt"): + try: + Path( + self._organizer.modList() + .getMod(mod) + .absolutePath(), + enabled_txt.path("/"), + ).unlink() + self._organizer.modDataChanged( + self._organizer.modList().getMod(mod) + ) + except FileNotFoundError: + pass + + game = self._organizer.managedGame() + if isinstance(game, OblivionRemasteredGame): + if game.ue4ssDirectory().exists(): + for dir_info in game.ue4ssDirectory().entryInfoList( + QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot + ): + if QFileInfo( + QDir(dir_info.absoluteFilePath()).absoluteFilePath( + "scripts/main.lua" + ) + ).exists(): + mod_list.add(dir_info.fileName()) + if QFileInfo( + QDir(dir_info.absoluteFilePath()).absoluteFilePath( + "enabled.txt" + ) + ).exists(): + Path(dir_info.absoluteFilePath(), "enabled.txt").unlink() + + final_list = sorted(mod_list, key=cmp_to_key(self.sort_mods)) + self._model.setStringList(final_list) + + def write_mod_list(self): + mod_list: list[UE4SSModInfo] = [] + profile = QDir(self._organizer.profilePath()) + mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + with open(mods_txt.absoluteFilePath(), "w") as txt_file: + for i in range(self._model.rowCount()): + item = self._model.index(i, 0) + name = self._model.data(item, Qt.ItemDataRole.DisplayRole) + active = ( + self._model.data(item, Qt.ItemDataRole.CheckStateRole) + == Qt.CheckState.Checked + ) + mod_list.append({"mod_name": name, "mod_enabled": active}) + txt_file.write(f"{name} : {1 if active else 0}\n") + mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + with open(mods_json.absoluteFilePath(), "w") as json_file: + json_file.write(json.dumps(mod_list, indent=4)) + + def sort_mods(self, mod_a: str, mod_b: str) -> int: + profile = QDir(self._organizer.profilePath()) + mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + mods_list: list[str] = [] + if mods_json.exists() and mods_json.isFile(): + with open(mods_json.absoluteFilePath(), "r") as json_file: + try: + mods = json.load(json_file) + except JSONDecodeError: + mods = DEFAULT_UE4SS_MODS + for mod in mods: + if mod["mod_enabled"]: + mods_list.append(mod["mod_name"]) + index_a = -1 + if mod_a in mods_list: + index_a = mods_list.index(mod_a) + index_b = -1 + if mod_b in mods_list: + index_b = mods_list.index(mod_b) + if index_a != -1 and index_b != -1: + return index_a - index_b + if index_a != -1: + return -1 + if index_b != -1: + return 1 + if mod_a < mod_b: + return -1 + return 1 diff --git a/libs/basic_games/games/quarantine/game_masseffectlegendary.py b/libs/basic_games/games/quarantine/game_masseffectlegendary.py new file mode 100644 index 0000000..fadc250 --- /dev/null +++ b/libs/basic_games/games/quarantine/game_masseffectlegendary.py @@ -0,0 +1,24 @@ +from ...basic_game import BasicGame + + +class MassEffectLegendaryGame(BasicGame): + Name = "Mass Effect Legendary Edition Support Plugin" + Author = "LostDragonist" + Version = "1.0.0" + + GameName = "Mass Effect: Legendary Edition" + GameShortName = "masseffectlegendaryedition" + GameBinary = "Game/Launcher/MassEffectLauncher.exe" + GameLauncher = "MassEffectLauncher.exe" + GameDataPath = "%GAME_PATH%" + GameDocumentsDirectory = ( + "%USERPROFILE%/Documents/BioWare/Mass Effect Legendary Edition/Save" + ) + GameSaveExtension = "pcsav" + GameSteamId = 1328670 + GameOriginWatcherExecutables = ( + "masseffectlauncher.exe", + "masseffect1.exe", + "masseffect2.exe", + "masseffect3.exe", + ) diff --git a/libs/basic_games/games/quarantine/readme.txt b/libs/basic_games/games/quarantine/readme.txt new file mode 100644 index 0000000..7b7cd32 --- /dev/null +++ b/libs/basic_games/games/quarantine/readme.txt @@ -0,0 +1,3 @@ +These are plugins that we want to keep around for various reasons but don't want enabled in a default install. + +Mass Effect: Removed because it only supports like 1% of mods and is meant purely for possible WJ support. diff --git a/libs/basic_games/games/stalkeranomaly/XRIO.py b/libs/basic_games/games/stalkeranomaly/XRIO.py new file mode 100644 index 0000000..870f28d --- /dev/null +++ b/libs/basic_games/games/stalkeranomaly/XRIO.py @@ -0,0 +1,148 @@ +# -*- encoding: utf-8 -*- +from __future__ import annotations + +import io +import struct +from typing import Optional, Tuple + +from .XRMath import IVec3 + + +class XRReader: + def __init__(self, buffer: bytes): + self._buffer = buffer + self._pos = 0 + + def __len__(self) -> int: + return len(self._buffer) + + def _read(self, size: int) -> Tuple[bytes, int]: + pos = min(len(self._buffer), self._pos + size) + buffer = self._buffer[self._pos : pos] + return (buffer, pos) + + def read(self, size: int = -1) -> bytes: + if size < 0: + size = len(self._buffer) + if len(self._buffer) <= self._pos: + return b"" + (buffer, pos) = self._read(size) + self._pos = pos + return buffer + + def peek(self, size: int = -1) -> bytes: + if size < 0: + size = len(self._buffer) + if len(self._buffer) <= self._pos: + return b"" + (buffer, _pos) = self._read(size) + return buffer + + def seek(self, pos: int, whence: int = io.SEEK_SET) -> int: + if whence == 0: + if pos < 0: + raise ValueError(f"negative seek position {pos}") + self._pos = pos + elif whence == 1: + self._pos = max(0, self._pos + pos) + elif whence == 2: + self._pos = max(0, len(self._buffer) + pos) + else: + raise ValueError("unsupported whence value") + return self._pos + + def elapsed(self) -> int: + return len(self._buffer) - self._pos + + def eof(self) -> bool: + return self.elapsed() <= 0 + + def u8(self) -> int: + return int(struct.unpack(" int: + return int(struct.unpack(" int: + return int(struct.unpack(" int: + return int(struct.unpack(" int: + return int(struct.unpack(" int: + return int(struct.unpack(" int: + return int(struct.unpack(" int: + return int(struct.unpack(" bool: + return bool(struct.unpack(" float: + return float(struct.unpack(" str: + chars = bytearray() + while not self.eof(): + c = self.read(1) + if c == b"\x00": + return str(chars, "utf-8") + else: + chars += c + return "" + + def fvec3(self) -> IVec3: + (f1, f2, f3) = struct.unpack(" Optional[int]: + dw_type = 0 + dw_size = 0 + success = False + if self.last_pos != 0: + self.seek(self.last_pos) + dw_type = self.u32() + dw_size = self.u32() + if (dw_type & (~(1 << 31))) == id: + success = True + + if not success: + self.seek(0) + while not self.eof(): + dw_type = self.u32() + dw_size = self.u32() + if (dw_type & (~(1 << 31))) == id: + success = True + break + else: + self.seek(dw_size, io.SEEK_CUR) + + if not success: + self.last_pos = 0 + return None + + if (self._pos + dw_size) < len(self._buffer): + self.last_pos = self._pos + dw_size + else: + self.last_pos = 0 + + return dw_size + + def open_chunk(self, id: int) -> Optional[XRStream]: + size = self.find_chunk(id) + if size and size != 0: + data = self.read(size) + return XRStream(data) + return None diff --git a/libs/basic_games/games/stalkeranomaly/XRMath.py b/libs/basic_games/games/stalkeranomaly/XRMath.py new file mode 100644 index 0000000..355fcaf --- /dev/null +++ b/libs/basic_games/games/stalkeranomaly/XRMath.py @@ -0,0 +1,37 @@ +class IVec3: + def __init__(self, x: float, y: float, z: float): + self.x = x + self.y = y + self.z = z + + def __str__(self) -> str: + return f"{self.x}, {self.y}, {self.z}" + + +class IVec4(IVec3): + def __init__(self, x: float, y: float, z: float, w: float): + super().__init__(x, y, z) + self.w = w + + def __str__(self) -> str: + return f"{self.x}, {self.y}, {self.z}, f{self.w}" + + +class IFlag: + def __init__(self, flag: int): + self._flag = flag + + def __str__(self) -> str: + return str(self._flag) + + def assign(self, mask: int): + self._flag = mask + + def has(self, mask: int) -> bool: + return bool((self._flag & mask) == mask) + + def set(self, mask: int) -> None: + self._flag |= mask + + def remove(self, mask: int) -> None: + self._flag &= ~mask diff --git a/libs/basic_games/games/stalkeranomaly/XRNET.py b/libs/basic_games/games/stalkeranomaly/XRNET.py new file mode 100644 index 0000000..2df87a9 --- /dev/null +++ b/libs/basic_games/games/stalkeranomaly/XRNET.py @@ -0,0 +1,45 @@ +from .XRIO import XRReader +from .XRMath import IVec3, IVec4 + + +class XRNETState: + def __init__(self): + self.position = IVec3(0.0, 0.0, 0.0) + self.quaternion = IVec4(0.0, 0.0, 0.0, 0.0) + self.enabled = False + + def read(self, reader: XRReader, fmin: IVec3, fmax: IVec3): + self.position = self.fvec_q8(reader, fmin, fmax) + self.quaternion = self.fqt_q8(reader) + self.enabled = bool(reader.u8()) + + def clamp(self, val: float, low: float, high: float) -> float: + if val < low: + return low + elif val > high: + return high + else: + return val + + def fvec_q8(self, reader: XRReader, fmin: IVec3, fmax: IVec3): + vec = IVec3(0.0, 0.0, 0.0) + vec.x = self.f_q8(reader, fmin.x, fmax.x) + vec.y = self.f_q8(reader, fmin.y, fmax.y) + vec.z = self.f_q8(reader, fmin.z, fmax.z) + vec.x = self.clamp(vec.x, fmin.x, fmax.x) + vec.y = self.clamp(vec.x, fmin.y, fmax.y) + vec.z = self.clamp(vec.z, fmin.z, fmax.z) + + def fqt_q8(self, reader: XRReader): + vec = IVec4(0.0, 0.0, 0.0, 0.0) + vec.x = self.f_q8(reader, -1.0, 1.0) + vec.y = self.f_q8(reader, -1.0, 1.0) + vec.z = self.f_q8(reader, -1.0, 1.0) + vec.w = self.f_q8(reader, -1.0, 1.0) + vec.x = self.clamp(vec.x, -1.0, 1.0) + vec.y = self.clamp(vec.y, -1.0, 1.0) + vec.z = self.clamp(vec.z, -1.0, 1.0) + vec.w = self.clamp(vec.w, -1.0, 1.0) + + def f_q8(self, reader: XRReader, fmin: float, fmax: float): + return (float(reader.u8()) / 255.0) * (fmax - fmin) + fmin diff --git a/libs/basic_games/games/stalkeranomaly/XRObject.py b/libs/basic_games/games/stalkeranomaly/XRObject.py new file mode 100644 index 0000000..d1eccdb --- /dev/null +++ b/libs/basic_games/games/stalkeranomaly/XRObject.py @@ -0,0 +1,277 @@ +# -*- encoding: utf-8 -*- + +import io +from enum import IntFlag +from typing import List + +from .XRIO import XRReader +from .XRMath import IFlag, IVec3 +from .XRNET import XRNETState + + +class XRFlag(IntFlag): + CHUNK_ALIFE = 0x0 + CHUNK_SPAWN = 0x1 + CHUNK_OBJECT = 0x2 + CHUNK_GAME_TIME = 0x5 + CHUNK_REGISTRY = 0x9 + + SPAWN_VERSION = 1 << 5 + + MSG_UPDATE = 0x0 + MSG_SPAWN = 0x1 + + TRADER_INFINITE_AMMO = 0x1 + + +class XRAbstract: + def __init__(self, name: str = ""): + self._valid = False + self.name = name + self.name_replace = "" + self.rp = 0xFE + self.position = IVec3(0.0, 0.0, 0.0) + self.angle = IVec3(0.0, 0.0, 0.0) + self.respawn_time = 0 + self.id = 0xFFFF + self.id_parent = 0xFFFF + self.id_phantom = 0xFFFF + self.flags = IFlag(0) + self.version = 0 + self.game_type = IFlag(0) + self.script_version = 0 + self.client_data: List[int] = [] + self.spawn_id = 0 + self.ini_str = "" + + def read_spawn(self, reader: XRReader): + spawn = reader.u16() + if spawn != XRFlag.MSG_SPAWN: + self._valid = False + return + self.name = reader.str() + self.name2 = reader.str() + reader.seek(1, io.SEEK_CUR) # temp_gt + self.rp = reader.u8() + self.position = reader.fvec3() + self.angle = reader.fvec3() + self.respawn_time = reader.u16() + self.id = reader.u16() + self.id_parent = reader.u16() + self.id_phantom = reader.u16() + self.flags = IFlag(reader.u16()) + if self.flags.has(XRFlag.SPAWN_VERSION): + self.version = reader.u16() + if self.version == 0: + reader._pos -= 2 # pyright: ignore[reportPrivateUsage] + return + if self.version > 120: + self.game_type.set(reader.u16()) + if self.version > 69: + self.script_version = reader.u16() + if self.version > 70: + cl_size = 0 + if self.version > 93: + cl_size = reader.u16() + else: + cl_size = reader.u8() + if cl_size > 0: + for _ in range(cl_size): + data = reader.u8() + self.client_data.append(data) + if self.version > 79: + self.spawn_id = reader.u16() + self._valid = True + + def __bool__(self): + return self._valid + + +class XRVisual: + def __init__(self): + self.visual_name = "" + self.startup_animation = "" + self.flags = IFlag(0) + + def read_visual(self, reader: XRReader, version: int): + self.visual_name = reader.str() + self.flags = IFlag(reader.u8()) + + +class XRBoneData: + def __init__(self): + self.bones_mask = -1 + self.root_bone = 0 + self.min = IVec3(0.0, 0.0, 0.0) + self.max = IVec3(0.0, 0.0, 0.0) + self.bones: list[XRNETState] = [] + + def load(self, reader: XRReader): + self.bones_mask = reader.u64() + self.root_bone = reader.u16() + self.min = reader.fvec3() + self.max = reader.fvec3() + bones_count = reader.u16() + for _ in range(bones_count): + bone = XRNETState() + bone.read(reader, self.min, self.max) + self.bones.append(bone) + + +class XRSkeleton: + def __init__(self): + self.source_id = -1 + self.saved_bones = XRBoneData() + + def read_state(self, reader: XRReader): + self.visual_animation = reader.str() + flags = IFlag(reader.u8()) + self.source_id = reader.u16() + if flags.has(4): + self.saved_bones.load(reader) + + +class XRObject(XRAbstract): + def __init__(self): + super().__init__() + self.graph_id = 0 + self.distance = 0.0 + self.direct_control = True + self.node_id = 0 + self.story_id = -1 + self.spawn_story_id = -1 + + def read_spawn(self, reader: XRReader): + super().read_spawn(reader) + size = reader.u16() + if size <= 2: + self._valid = False + return + self.read_state(reader) + + def read_state(self, reader: XRReader): + self.graph_id = reader.u16() + self.distance = reader.float() + self.direct_control = bool(reader.u32()) + self.node_id = reader.u32() + self.flags.set(reader.u32()) + self.ini_str = reader.str() + self.story_id = reader.u32() + self.spawn_story_id = reader.u32() + + def read_update(self, reader: XRReader): + update = reader.u16() + if update != XRFlag.MSG_UPDATE: + self._valid = False + return + + +class XRDynamicObject(XRObject): + def __init__(self): + super().__init__() + self.time_id = -1 + self.switch_counter = 0 + + +class XRDynamicObjectVisual(XRDynamicObject, XRVisual): + def read_state(self, reader: XRReader): + XRDynamicObject.read_state(self, reader) + if self.version > 31: + XRVisual.read_visual(self, reader, self.version) + + +class XRCreatureAbstract(XRDynamicObjectVisual): + def __init__(self): + super().__init__() + self.team = 0 + self.squad = 0 + self.group = 0 + self.health = 1.0 + self.dynamic_out: list[int] = [] + self.dynamic_in: list[int] = [] + self.killer_id = -1 + self.death_time = 0 + + def read_state(self, reader: XRReader): + super().read_state(reader) + self.team = reader.u8() + self.squad = reader.u8() + self.group = reader.u8() + self.health = reader.float() * 100 + + for _ in range(reader.u32()): + _id = reader.u16() + self.dynamic_out.append(_id) + + for _ in range(reader.u32()): + _id = reader.u16() + self.dynamic_in.append(_id) + + self.killer_id = reader.u16() + self.death_time = reader.u64() + + +class XRTraderAbstract: + def __init__(self): + self.money = 0 + self.max_item_mass = 0 + self.character_name = "" + self.character_name_str = "" + self.character_profile = "" + self.specific_character = "" + self.community_index = -1 + self.rank = -1 # -MAX_INT? + self.reputation = -1 # -MAX_INT? + self.dead_body_can_take = True + self.dead_body_closed = False + self.trader_flags = IFlag(0) + self.trader_flags.remove(XRFlag.TRADER_INFINITE_AMMO) + + def read_state(self, reader: XRReader): + self.money = reader.u32() + self.specific_character = reader.str() + self.trader_flags.assign(reader.u32()) + self.character_profile = reader.str() + self.community_index = reader.s32() + self.rank = reader.s32() + self.reputation = reader.s32() + self.character_name_str = reader.str() + self.dead_body_can_take = reader.u8() == 1 + self.dead_body_closed = reader.u8() == 1 + + +class XRCreatureActor(XRCreatureAbstract, XRTraderAbstract, XRSkeleton): + def __init__(self): + XRCreatureAbstract.__init__(self) + XRTraderAbstract.__init__(self) + XRSkeleton.__init__(self) + self.state = 0 + self.acceleration = IVec3(0.0, 0.0, 0.0) + self.velocity = IVec3(0.0, 0.0, 0.0) + self.radiation = 0.0 + self.weapon = 0 + self.num_items = 0 + self.holder_id = -1 + + def read_spawn(self, reader: XRReader): + XRCreatureAbstract.read_spawn(self, reader) + + def read_state(self, reader: XRReader): + XRCreatureAbstract.read_state(self, reader) + XRTraderAbstract.read_state(self, reader) + XRSkeleton.read_state(self, reader) + self.holder_id = reader.u16() + + def read_update(self, reader: XRReader): + XRCreatureAbstract.read_update(self, reader) + # XRTraderAbstract.read_update(self, reader) # Future? + self.state = reader.u16() + # acceleration (r_sdir) + reader.seek(2, io.SEEK_CUR) # u16 + reader.seek(4, io.SEEK_CUR) # float + # velocity (r_sdir) + reader.seek(2, io.SEEK_CUR) # u16 + reader.seek(4, io.SEEK_CUR) # float + self.radiation = reader.float() + self.weapon = reader.u8() + self.num_items = reader.u16() diff --git a/libs/basic_games/games/stalkeranomaly/XRSave.py b/libs/basic_games/games/stalkeranomaly/XRSave.py new file mode 100644 index 0000000..b73a988 --- /dev/null +++ b/libs/basic_games/games/stalkeranomaly/XRSave.py @@ -0,0 +1,164 @@ +import io +import struct +from datetime import datetime +from pathlib import Path +from typing import BinaryIO, Optional, cast + +import lzokay # pyright: ignore[reportMissingTypeStubs] + +from .XRIO import XRReader, XRStream +from .XRObject import XRCreatureActor, XRFlag + + +class XRSave: + filepath: Path + player: XRCreatureActor + + _factions = { + 0: "Loner", + 1: "Monster", + 2: "Trader", + 3: "Army", + 4: "Sin", + 5: "Bandit", + 6: "Duty", + 7: "Ecologist", + 8: "Freedom", + 9: "Mercenary", + 10: "Army", + 11: "Monolith", + 12: "Sin", + 13: "Loner", + 14: "Zombified", + 15: "Clear Sky", + 16: "UNISG", + 17: "Renegade", + 18: "Loner", + 19: "Bandit", + 20: "Duty", + 21: "Freedom", + 22: "Clear Sky", + 23: "Ecologist", + 24: "Mercenary", + 25: "Military", + 26: "Monolith", + 27: "Zombified", + 28: "Sin", + 29: "UNISG", + 30: "Renegade", + 31: "Participant", + } + + _ranks = { + 1999: "Novice", + 3999: "Trainee", + 6999: "Experienced", + 9999: "Professional", + 14999: "Veteran", + 20999: "Expert", + 27999: "Master", + "max": "Legend", + } + + _reputation = { + -2000: "Terrible", + -1500: "Really Bad", + -1000: "Very Bad", + -500: "Bad", + 499: "Netural", + 999: "Good", + 1499: "Very Good", + 1999: "Really Good", + "max": "Excellent", + } + + def __init__(self, filepath: Path): + self.filepath = filepath + self.fetchInfo() + with open(filepath, "rb") as file: + stream = self.readFile(file) + if stream: + self.readObject(stream) + + def fetchInfo(self): + self.splitInfo() + self.time = self.filepath.stat().st_mtime + self.time_date = datetime.fromtimestamp(self.time) + self.time_fmt = self.time_date.strftime("%I:%M %m/%d/%Y") + + def splitInfo(self): + save_clean = self.filepath.name.split(".scop", 1)[0] + save_split = save_clean.split(" - ", 1) + self.user = save_split[0] + if len(save_split) > 1: + save_end = save_split[1].split("_", 1) + if len(save_end) > 1: + self.save_fmt = f"{save_end[0]} (#{save_end[1]})".title() + else: + self.save_fmt = f"{save_end[0]}".title() + else: + self.save_fmt = "Unknown" + + def readFile(self, file: BinaryIO) -> Optional[XRStream]: + size = self.filepath.stat().st_size + if size < 8: + return None + + (start, version, source) = struct.unpack("@iii", file.read(12)) + if (start == -1) and (version >= 6): + file.seek(12) + data = file.read(size - 12) + return XRStream( + cast( + bytes, + lzokay.decompress( # pyright: ignore[reportUnknownMemberType] + data, source + ), + ) + ) + + return None + + def readObject(self, stream: XRStream): + chunk = stream.open_chunk(XRFlag.CHUNK_OBJECT) + if chunk: + chunk.seek(4, io.SEEK_CUR) # obj_count + count_spawn = chunk.u16() + spawn = XRReader(chunk.read(count_spawn)) + actor = XRCreatureActor() + actor.read_spawn(spawn) + count_update = chunk.u16() + update = XRReader(chunk.read(count_update)) + actor.read_update(update) + if actor: + self.player = actor + return None + + def getFaction(self) -> str: + player = self.player + if player: + player_faction = player.community_index + for faction in self._factions: + if player_faction == faction: + return self._factions[faction] + return "Unknown" + + def getRank(self) -> str: + player = self.player + if player: + player_rank = player.rank + for rank in self._ranks: + if isinstance(rank, int): + if player_rank <= rank: + return self._ranks[rank] + return self._ranks["max"] + + def getReputation(self) -> str: + player = self.player + if player: + player_rep = player.reputation + for rep in self._reputation: + if isinstance(rep, int): + if player_rep <= rep: + return self._reputation[rep] + return self._reputation["max"] diff --git a/libs/basic_games/games/stalkeranomaly/__init__.py b/libs/basic_games/games/stalkeranomaly/__init__.py new file mode 100644 index 0000000..a8730a4 --- /dev/null +++ b/libs/basic_games/games/stalkeranomaly/__init__.py @@ -0,0 +1,38 @@ +from .XRIO import XRReader, XRStream +from .XRMath import IFlag, IVec3, IVec4 +from .XRNET import XRNETState +from .XRObject import ( + XRAbstract, + XRBoneData, + XRCreatureAbstract, + XRCreatureActor, + XRDynamicObject, + XRDynamicObjectVisual, + XRFlag, + XRObject, + XRSkeleton, + XRTraderAbstract, + XRVisual, +) +from .XRSave import XRSave + +__all__ = [ + "IFlag", + "IVec3", + "IVec4", + "XRAbstract", + "XRBoneData", + "XRCreatureAbstract", + "XRCreatureActor", + "XRDynamicObject", + "XRDynamicObjectVisual", + "XRFlag", + "XRNETState", + "XRObject", + "XRReader", + "XRSave", + "XRSkeleton", + "XRStream", + "XRTraderAbstract", + "XRVisual", +] diff --git a/libs/basic_games/gog_utils.py b/libs/basic_games/gog_utils.py new file mode 100644 index 0000000..e5e6d24 --- /dev/null +++ b/libs/basic_games/gog_utils.py @@ -0,0 +1,35 @@ +# Code adapted from EzioTheDeadPoet / erri120: +# https://github.com/ModOrganizer2/modorganizer-basic_games/pull/5 + +import winreg +from pathlib import Path + + +def find_games() -> dict[str, Path]: + # List the game IDs from the registry: + game_ids: list[str] = [] + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, r"Software\Wow6432Node\GOG.com\Games" + ) as key: + nkeys = winreg.QueryInfoKey(key)[0] + for ik in range(nkeys): + game_key = winreg.EnumKey(key, ik) + if game_key.isdigit(): + game_ids.append(game_key) + except FileNotFoundError: + return {} + + # For each game, query the path: + games: dict[str, Path] = {} + for game_id in game_ids: + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + f"Software\\Wow6432Node\\GOG.com\\Games\\{game_id}", + ) as key: + games[game_id] = Path(winreg.QueryValueEx(key, "path")[0]) + except FileNotFoundError: + pass + + return games diff --git a/libs/basic_games/origin_utils.py b/libs/basic_games/origin_utils.py new file mode 100644 index 0000000..da050e8 --- /dev/null +++ b/libs/basic_games/origin_utils.py @@ -0,0 +1,104 @@ +# -*- encoding: utf-8 -*- + +# Heavily influenced by https://github.com/erri120/GameFinder + +import os +import threading +import time +from collections.abc import Sequence +from pathlib import Path +from urllib import parse + +import psutil + + +class OriginWatcher: + """ + This is a class to control killing Origin when needed. This is used in + order to hook and unhook Origin to get around the Origin DRM. Support + for launching Origin is not included as it's intended for the game's + DRM to launch Origin as needed. + """ + + def __init__(self, executables: Sequence[str] = []): + self.executables = list(map(lambda s: s.lower(), executables)) + + def spawn_origin_watcher(self) -> bool: + self.kill_origin() + self.worker_alive = True + self.worker = threading.Thread(target=self._workerFunc) + self.worker.start() + return True + + def stop_origin_watcher(self) -> None: + self.worker_alive = False + self.worker.join(10.0) + + def kill_origin(self) -> None: + """ + Kills the Origin application + """ + for proc in psutil.process_iter(): + if proc.name().lower() == "origin.exe": + proc.kill() + + def _workerFunc(self) -> None: + gameAliveCount = 300 # Large number to allow Origin and the game to launch + while self.worker_alive: + gameAlive = False + # See if the game is still alive + for proc in psutil.process_iter(): + if proc.name().lower() in self.executables: + gameAlive = True + break + if gameAlive: + # Game is alive, sleep and keep monitoring at faster pace + gameAliveCount = 5 + else: + gameAliveCount -= 1 + if gameAliveCount <= 0: + self.kill_origin() + self.worker_alive = False + time.sleep(1) + + +def find_games() -> dict[str, Path]: + """ + Find the list of Origin games installed. + + Returns: + A mapping from Origin manifest IDs to install locations for available + Origin games. + """ + games: dict[str, Path] = {} + + program_data_path = os.path.expandvars("%PROGRAMDATA%") + local_content_path = Path(program_data_path).joinpath("Origin", "LocalContent") + for manifest in local_content_path.glob("**/*.mfst"): + # Skip any manifest file with '@steam' + if "@steam" in manifest.name.lower(): + continue + + # Read the file and look for &id= and &dipinstallpath= + with open(manifest, "r") as f: + manifest_query = f.read() + url = parse.urlparse(manifest_query) + query = parse.parse_qs(url.query) + if "id" not in query: + # If id is not present, we have no clue what to do. + continue + if "dipinstallpath" not in query: + # We could query the Origin server for the install location but... no? + continue + + for id_ in query["id"]: + for path_ in query["dipinstallpath"]: + games[id_] = Path(path_) + + return games + + +if __name__ == "__main__": + games = find_games() + for k, v in games.items(): + print("Found game with id {} at {}.".format(k, v)) diff --git a/libs/basic_games/plugin-requirements.txt b/libs/basic_games/plugin-requirements.txt new file mode 100644 index 0000000..f945efd --- /dev/null +++ b/libs/basic_games/plugin-requirements.txt @@ -0,0 +1,4 @@ +psutil==5.8.0 +vdf==3.4 +lzokay==1.1.5 +pyyaml==6.0.2 diff --git a/libs/basic_games/poetry.lock b/libs/basic_games/poetry.lock new file mode 100644 index 0000000..a4066dd --- /dev/null +++ b/libs/basic_games/poetry.lock @@ -0,0 +1,337 @@ +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. + +[[package]] +name = "lzokay" +version = "1.1.5" +description = "Python bindings for LZ👌, a LZO compression/decompression algorithm." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "lzokay-1.1.5-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:4faeefdef8132c4db995de8e96a649c890c63a81cee06b3bbcae9d224302fa09"}, + {file = "lzokay-1.1.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ee2c7144dd3916cf0392d3851cebe8f28136e7998b7a50a91f63d7276327d70"}, + {file = "lzokay-1.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:829bfd081c8f03e85994d920ce7cc8d45033d7c467fc2fadc17b5c47667ec045"}, + {file = "lzokay-1.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d6b1d80121cdbc3cb106b3fafc03b139f3de3c134ce58b38fcc2d751fc5b013"}, + {file = "lzokay-1.1.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f07bdd8c3bd9443d3f5ca69a03b9dc35522c5c92fce61db087ded1f348274f"}, + {file = "lzokay-1.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:266db5697a0d01428e4c31633f8dc1e87666ad17bb46b4c48ec7b7b8b70b94bc"}, + {file = "lzokay-1.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:eec7679194643bc1417f8883ef3c9d27e95e00632d4d2a7dc9ef1a1ca92cdd95"}, + {file = "lzokay-1.1.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5426a80664c471c18a47766111629cae5c9435ca8421cd208b00b10de5df5df9"}, + {file = "lzokay-1.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:a4df719d5fef922e0f43ec2408cd70280171ded18ab096c1e88fd1c3ca4dff46"}, + {file = "lzokay-1.1.5-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:88fa389193cde7feb13fd4b57b6b8d626a6159bfe84cd795e664e0a738debb42"}, + {file = "lzokay-1.1.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c83a0f1cc7628237466c298e676a64a1ab4914ccf628b3775f8680477b77481"}, + {file = "lzokay-1.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:f1997c6994239ea24a2c7c7812f9e06bceb6b216c5537b85d8b585dbfd711cb0"}, + {file = "lzokay-1.1.5-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:4389b8b6f3c95aaa33308e2129b026140c850d94440fbea4fdeabcd42477f3b2"}, + {file = "lzokay-1.1.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:867a9fd4a76c830b17366fd2e8bd03fe9b29cf5c13c42bc19562e411cf648dad"}, + {file = "lzokay-1.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:f828864453ddfca036de7470da74245d8dcf369c521291037b1b01ac7c5dbd83"}, + {file = "lzokay-1.1.5.tar.gz", hash = "sha256:3c2e81d178161de58bf233eec87c6ef3dba96fecdd9ba72f8f2c8790adc18f74"}, +] + +[package.extras] +test = ["pytest"] + +[[package]] +name = "mobase-stubs" +version = "2.5.2" +description = "PEP561 stub files for the mobase Python API." +optional = false +python-versions = "<4.0,>=3.12" +groups = ["dev"] +files = [ + {file = "mobase_stubs-2.5.2-py3-none-any.whl", hash = "sha256:6a2c1c95494e7dc0d0f51d4f8f178423c0d9904f823f3c44845315bff304b948"}, + {file = "mobase_stubs-2.5.2.tar.gz", hash = "sha256:70c15579828df3bce01746e51fee07176390cdc386facea2b156ea04b3de055c"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] + +[[package]] +name = "poethepoet" +version = "0.36.0" +description = "A task runner that works well with poetry and uv." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "poethepoet-0.36.0-py3-none-any.whl", hash = "sha256:693e3c1eae9f6731d3613c3c0c40f747d3c5c68a375beda42e590a63c5623308"}, + {file = "poethepoet-0.36.0.tar.gz", hash = "sha256:2217b49cb4e4c64af0b42ff8c4814b17f02e107d38bc461542517348ede25663"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +pyyaml = ">=6.0.2,<7.0" + +[package.extras] +poetry-plugin = ["poetry (>=1.2.0,<3.0.0) ; python_version < \"4.0\""] + +[[package]] +name = "psutil" +version = "5.9.8" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +groups = ["main"] +files = [ + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +] + +[package.extras] +test = ["enum34 ; python_version <= \"3.4\"", "ipaddress ; python_version < \"3.0\"", "mock ; python_version < \"3.0\"", "pywin32 ; sys_platform == \"win32\"", "wmi ; sys_platform == \"win32\""] + +[[package]] +name = "pyqt6" +version = "6.7.0" +description = "Python bindings for the Qt cross platform application toolkit" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "PyQt6-6.7.0-1-cp38-abi3-macosx_10_14_universal2.whl", hash = "sha256:656734112853fde1be0963f0ad362e5efd87ba6c6ff234cb1f9fe8003ee254e6"}, + {file = "PyQt6-6.7.0-1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fa2d27fc2f5340f3f1e145c815101ef4550771a9e4bfafd4c7c2479fe83d9488"}, + {file = "PyQt6-6.7.0-1-cp38-abi3-win_amd64.whl", hash = "sha256:6a1f6dfe03752f888b5e628c208f9fd1a03bda7ebda59ffed8c13580289a1892"}, + {file = "PyQt6-6.7.0-cp38-abi3-macosx_10_14_universal2.whl", hash = "sha256:919ffb01020ece42209228bf94b4f2c156a6b77cc5a69a90a05e358b0333750b"}, + {file = "PyQt6-6.7.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e294f025f94493ee12b66efd6893fab309c9063172bb8a5b184f84dfc1ebcc49"}, + {file = "PyQt6-6.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:9d8865fb6357dba032002c4554a9648e88f2b4706c929cc51fba58edafad91fc"}, + {file = "PyQt6-6.7.0.tar.gz", hash = "sha256:3d31b2c59dc378ee26e16586d9469842483588142fc377280aad22aaf2fa6235"}, +] + +[package.dependencies] +PyQt6-Qt6 = ">=6.7.0,<6.8.0" +PyQt6-sip = ">=13.6,<14" + +[[package]] +name = "pyqt6-qt6" +version = "6.7.3" +description = "The subset of a Qt installation needed by PyQt6." +optional = false +python-versions = "*" +groups = ["main", "dev"] +files = [ + {file = "PyQt6_Qt6-6.7.3-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:f517a93b6b1a814d4aa6587adc312e812ebaf4d70415bb15cfb44268c5ad3f5f"}, + {file = "PyQt6_Qt6-6.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8551732984fb36a5f4f3db51eafc4e8e6caf18617365830285306f2db17a94c2"}, + {file = "PyQt6_Qt6-6.7.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:50c7482bcdcf2bb78af257fb10ed8b582f8daf91d829782393bc50ac5a0a900c"}, + {file = "PyQt6_Qt6-6.7.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:cb525fdd393332de60887953029276a44de480fce1d785251ae639580f5e7246"}, + {file = "PyQt6_Qt6-6.7.3-py3-none-win_amd64.whl", hash = "sha256:36ea0892b8caeb983af3f285f45fb8dfbb93cfd972439f4e01b7efb2868f6230"}, +] + +[[package]] +name = "pyqt6-sip" +version = "13.10.0" +description = "The sip module support for PyQt6" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "PyQt6_sip-13.10.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7b1258963717cfae1d30e262bb784db808072a8a674d98f57c2076caaa50499"}, + {file = "PyQt6_sip-13.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d27a3fed2a461f179d3cde6a74530fbad629ccaa66ed739b9544fda1932887af"}, + {file = "PyQt6_sip-13.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0422781c77b85eefd7a26f104c5998ede178a16b0fd35212664250215b6e5e4c"}, + {file = "PyQt6_sip-13.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f64183dde2af36515dab515f4301a5a8d9b3658b231769fa48fe6287dc52f375"}, + {file = "PyQt6_sip-13.10.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e78fb8036b18f6258a1af0956c5a3cec1dd3d8dd5196ecd89a31b529bf40e82"}, + {file = "PyQt6_sip-13.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e19d5887fa3003a635419644dfed3158cb15eb566fc27b1ed56913a5767a71dc"}, + {file = "PyQt6_sip-13.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:079bb946edc3960f08d92b3a8eebff55d3abb51bc2a0583b6683dfd9f77a616a"}, + {file = "PyQt6_sip-13.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:90974f5dbba1f5d1d2ca9b1cfdfd5258e5e3cfacead03f0df674d54c69973ea7"}, + {file = "PyQt6_sip-13.10.0-cp311-cp311-win_arm64.whl", hash = "sha256:bbefd5539eeda4dec37e8b6dfc362ba240ec31279060336bcceaff572807dac8"}, + {file = "PyQt6_sip-13.10.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:48791db2914fc39c3218519a02d2a5fd3fcd354a1be3141a57bf2880701486f2"}, + {file = "PyQt6_sip-13.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:466d6b4791973c9fcbdc2e0087ed194b9ea802a8c3948867a849498f0841c70c"}, + {file = "PyQt6_sip-13.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ae15358941f127cd3d1ab09c1ebd45c4dabb0b2e91587b9eebde0279d0039c54"}, + {file = "PyQt6_sip-13.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad573184fa8b00041944e5a17d150ab0d08db2d2189e39c9373574ebab3f2e58"}, + {file = "PyQt6_sip-13.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:2d579d810d0047d40bde9c6aef281d6ed218db93c9496ebc9e55b9e6f27a229d"}, + {file = "PyQt6_sip-13.10.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7b6e250c2e7c14702a623f2cc1479d7fb8db2b6eee9697cac10d06fe79c281bb"}, + {file = "PyQt6_sip-13.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fcb30756568f8cd59290f9ef2ae5ee3e72ff9cdd61a6f80c9e3d3b95ae676be"}, + {file = "PyQt6_sip-13.10.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:757ac52c92b2ef0b56ecc7cd763b55a62d3c14271d7ea8d03315af85a70090ff"}, + {file = "PyQt6_sip-13.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:571900c44a3e38738d696234d94fe2043972b9de0633505451c99e2922cb6a34"}, + {file = "PyQt6_sip-13.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:39cba2cc71cf80a99b4dc8147b43508d4716e128f9fb99f5eb5860a37f082282"}, + {file = "PyQt6_sip-13.10.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f026a1278f9c2a745542d4a05350f2392d4cf339275fb8efccb47b0f213d120"}, + {file = "PyQt6_sip-13.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:548c70bc40d993be0eb011e1bbc41ba7c95f6af375613b58217f39ad8d703345"}, + {file = "PyQt6_sip-13.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21417ffd2c489afef114cb09683bbc0fb24d78df848a21fc0d09e70ecbb0a4a4"}, + {file = "PyQt6_sip-13.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:6e1b1f7a29290afc83bcd9970e0cffa2d0da87d81796b6eab7b6f583e4f49652"}, + {file = "pyqt6_sip-13.10.0.tar.gz", hash = "sha256:d6daa95a0bd315d9ec523b549e0ce97455f61ded65d5eafecd83ed2aa4ae5350"}, +] + +[[package]] +name = "pyright" +version = "1.1.402" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982"}, + {file = "pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" +typing-extensions = ">=4.1" + +[package.extras] +all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] +nodejs = ["nodejs-wheel-binaries"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "ruff" +version = "0.12.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be"}, + {file = "ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e"}, + {file = "ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9"}, + {file = "ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da"}, + {file = "ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce"}, + {file = "ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d"}, + {file = "ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04"}, + {file = "ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342"}, + {file = "ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a"}, + {file = "ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639"}, + {file = "ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12"}, + {file = "ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e"}, +] + +[[package]] +name = "types-psutil" +version = "5.9.5.20240516" +description = "Typing stubs for psutil" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "types-psutil-5.9.5.20240516.tar.gz", hash = "sha256:bb296f59fc56458891d0feb1994717e548a1bcf89936a2877df8792b822b4696"}, + {file = "types_psutil-5.9.5.20240516-py3-none-any.whl", hash = "sha256:83146ded949a10167d9895e567b3b71e53ebc5e23fd8363eab62b3c76cce7b89"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "vdf" +version = "3.4" +description = "Library for working with Valve's VDF text format" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "vdf-3.4-py2.py3-none-any.whl", hash = "sha256:68c1a125cc49e343d535af2dd25074e9cb0908c6607f073947c4a04bbe234534"}, + {file = "vdf-3.4.tar.gz", hash = "sha256:fd5419f41e07a1009e5ffd027c7dcbe43d1f7e8ef453aeaa90d9d04b807de2af"}, +] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.12,<4.0" +content-hash = "b3a65da37845a56d4b259c4d73913b8957a61382499eeda8bdce2343a48e8c66" diff --git a/libs/basic_games/pyproject.toml b/libs/basic_games/pyproject.toml new file mode 100644 index 0000000..89778ca --- /dev/null +++ b/libs/basic_games/pyproject.toml @@ -0,0 +1,59 @@ +# note: this file is for local development, currently the file is not used to build +# the plugin +[project] +name = "basic-games" +version = "0.1.0" +description = "" +authors = [] +license = "MIT" +readme = "README.md" +requires-python = ">=3.12,<4.0" +dynamic = ["dependencies"] + +[tool.poetry] +package-mode = false + +[tool.poetry.dependencies] +psutil = "^5.8.0" +vdf = "3.4" +lzokay = "1.1.5" +pyqt6 = "6.7.0" +pyyaml = "^6.0.2" + +[tool.poetry.group.dev.dependencies] +mobase-stubs = "^2.5.2" +pyqt6 = "^6.7.0" +pyright = "^1.1.402" +ruff = "^0.12.2" +types-psutil = "<6" +poethepoet = "^0.36.0" + +[build-system] +requires = ["poetry-core (>=2.0)"] +build-backend = "poetry.core.masonry.api" + +[tool.poe.tasks] +format-imports = "ruff check --select I . --fix" +format-ruff = "ruff format ." +format.sequence = ["format-imports", "format-ruff"] +lint-ruff = "ruff check ." +lint-ruff-format = "ruff format --check ." +lint-pyright = "pyright ." +lint.sequence = ["lint-ruff", "lint-ruff-format", "lint-pyright"] +lint.ignore_fail = "return_non_zero" + +[tool.ruff.lint] +extend-select = ["B", "Q", "I"] + +[tool.ruff.lint.flake8-bugbear] +extend-immutable-calls = ["PyQt6.QtCore.QModelIndex"] + +[tool.ruff.lint.isort] +known-first-party = ["mobase"] + +[tool.pyright] +exclude = ["lib", "**/.*", "venv"] +typeCheckingMode = "strict" +reportMissingTypeStubs = true +reportMissingModuleSource = false +pythonPlatform = "Windows" diff --git a/libs/basic_games/steam_utils.py b/libs/basic_games/steam_utils.py new file mode 100644 index 0000000..d1d4a06 --- /dev/null +++ b/libs/basic_games/steam_utils.py @@ -0,0 +1,187 @@ +# Code greatly inspired by https://github.com/LostDragonist/steam-library-setup-tool + +import sys +import winreg +from pathlib import Path +from typing import TypedDict, cast + +import vdf # pyright: ignore[reportMissingTypeStubs] + + +class SteamGame: + def __init__(self, appid: str, installdir: str): + self.appid = appid + self.installdir = installdir + + def __repr__(self): + return str(self) + + def __str__(self): + return "{} ({})".format(self.appid, self.installdir) + + +class _AppState(TypedDict): + appid: str + installdir: str + + +class _AppManifest(TypedDict): + AppState: _AppState + + +class _LibraryFolder(TypedDict): + path: str + + +class _LibraryFolders(TypedDict, total=False): + libraryfolders: dict[str, _LibraryFolder] + LibraryFolders: dict[str, str] + + +class LibraryFolder: + def __init__(self, path: Path): + self.path = path + + self.games: list[SteamGame] = [] + for filepath in path.joinpath("steamapps").glob("appmanifest_*.acf"): + try: + with open(filepath, "r", encoding="utf-8") as fp: + info = cast( + _AppManifest, + vdf.load(fp), # pyright: ignore[reportUnknownMemberType] + ) + app_state = info["AppState"] + except KeyError: + print( + f'Unable to read application state from "{filepath}"', + file=sys.stderr, + ) + continue + except Exception as e: + print(f'Unable to parse file "{filepath}": {e}', file=sys.stderr) + continue + + try: + app_id = app_state["appid"] + install_dir = app_state["installdir"] + self.games.append(SteamGame(app_id, install_dir)) + except KeyError: + print( + f"Unable to read application ID or installation folder " + f'from "{filepath}"', + file=sys.stderr, + ) + continue + + def __repr__(self): + return str(self) + + def __str__(self): + return "LibraryFolder at {}: {}".format(self.path, self.games) + + +def parse_library_info(library_vdf_path: Path) -> list[LibraryFolder]: + """ + Read library folders from the main library file. + + Args: + library_vdf_path: The main library file (from the Steam installation + folder). + + Returns: + A list of LibraryFolder, for each library found. + """ + + with open(library_vdf_path, "r", encoding="utf-8") as f: + info = cast( + _LibraryFolders, + vdf.load(f), # pyright: ignore[reportUnknownMemberType] + ) + + info_folders: dict[str, str] | dict[str, _LibraryFolder] + + if "libraryfolders" in info: + # new format + info_folders = info["libraryfolders"] + + elif "LibraryFolders" in info: + # old format + info_folders = info["LibraryFolders"] + + else: + raise ValueError(f'Unknown file format from "{library_vdf_path}"') + + library_folders: list[LibraryFolder] = [] + + for key, value in info_folders.items(): + # only keys that are integer values contains library folder + try: + int(key) + except ValueError: + continue + + if isinstance(value, str): + path = value + else: + path = value["path"] + + try: + library_folders.append(LibraryFolder(Path(path))) + except Exception as e: + print( + 'Failed to read steam library from "{}", {}'.format(path, repr(e)), + file=sys.stderr, + ) + + return library_folders + + +def find_steam_path() -> Path | None: + """ + Retrieve the Steam path, if available. + + Returns: + The Steam path, or None if Steam is not installed. + """ + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Valve\\Steam") as key: + value = winreg.QueryValueEx(key, "SteamExe") + return Path(value[0].replace("/", "\\")).parent + except FileNotFoundError: + return None + + +def find_games() -> dict[str, Path]: + """ + Find the list of Steam games installed. + + Returns: + A mapping from Steam game ID to install locations for available + Steam games. + """ + steam_path = find_steam_path() + if not steam_path: + return {} + + library_vdf_path = steam_path.joinpath("steamapps", "libraryfolders.vdf") + + try: + library_folders = parse_library_info(library_vdf_path) + library_folders.append(LibraryFolder(steam_path)) + except FileNotFoundError: + return {} + + games: dict[str, Path] = {} + for library in library_folders: + for game in library.games: + games[game.appid] = Path(library.path).joinpath( + "steamapps", "common", game.installdir + ) + + return games + + +if __name__ == "__main__": + games = find_games() + for k, v in games.items(): + print("Found game with id {} at {}.".format(k, v)) diff --git a/libs/basic_games/vcpkg.json b/libs/basic_games/vcpkg.json new file mode 100644 index 0000000..a27b7c5 --- /dev/null +++ b/libs/basic_games/vcpkg.json @@ -0,0 +1,17 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": [ + "mo2-cmake" + ] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "69db61b22147b14ad66fd05cf798d855f6d68c12" + } + } +} diff --git a/libs/basic_games/winreg.py b/libs/basic_games/winreg.py new file mode 100644 index 0000000..e1303dc --- /dev/null +++ b/libs/basic_games/winreg.py @@ -0,0 +1,14 @@ +# Linux compatibility shim for Python plugins that import winreg. +# These plugins already handle FileNotFoundError fallback paths when registry +# lookups are unavailable. + +HKEY_LOCAL_MACHINE = object() +HKEY_CURRENT_USER = object() + + +def OpenKey(*_args, **_kwargs): + raise FileNotFoundError("winreg is not available on this platform") + + +def QueryValueEx(*_args, **_kwargs): + raise FileNotFoundError("winreg is not available on this platform") diff --git a/libs/bsa_extractor/.gitattributes b/libs/bsa_extractor/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/bsa_extractor/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/bsa_extractor/.github/workflows/build.yml b/libs/bsa_extractor/.github/workflows/build.yml new file mode 100644 index 0000000..ea613a5 --- /dev/null +++ b/libs/bsa_extractor/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build BSA Extractor + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build BSA Extractor + id: build-bsa-extractor + uses: ModOrganizer2/build-with-mob-action + with: + mo2-dependencies: uibase bsatk diff --git a/libs/bsa_extractor/.gitignore b/libs/bsa_extractor/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/bsa_extractor/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/bsa_extractor/.pre-commit-config.yaml b/libs/bsa_extractor/.pre-commit-config.yaml new file mode 100644 index 0000000..fd69da3 --- /dev/null +++ b/libs/bsa_extractor/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + # - repo: https://github.com/pre-commit/mirrors-clang-format + # rev: v19.1.5 + # hooks: + # - id: clang-format + # 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/bsa_extractor/CMakeLists.txt b/libs/bsa_extractor/CMakeLists.txt new file mode 100644 index 0000000..154e97a --- /dev/null +++ b/libs/bsa_extractor/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(bsa_extractor) + +add_subdirectory(src) diff --git a/libs/bsa_extractor/CMakePresets.json b/libs/bsa_extractor/CMakePresets.json new file mode 100644 index 0000000..023baf5 --- /dev/null +++ b/libs/bsa_extractor/CMakePresets.json @@ -0,0 +1,52 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg"], + "name": "vs2022-windows", + "toolset": "v143" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/bsa_extractor/src/CMakeLists.txt b/libs/bsa_extractor/src/CMakeLists.txt new file mode 100644 index 0000000..3a027c5 --- /dev/null +++ b/libs/bsa_extractor/src/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB bsa_extractor_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(bsa_extractor SHARED ${bsa_extractor_SOURCES}) +mo2_configure_plugin(bsa_extractor NO_SOURCES WARNINGS OFF) +target_link_libraries(bsa_extractor PRIVATE mo2::uibase mo2::bsatk) +mo2_install_plugin(bsa_extractor) diff --git a/libs/bsa_extractor/src/bsa_extractor_en.ts b/libs/bsa_extractor/src/bsa_extractor_en.ts new file mode 100644 index 0000000..7f27449 --- /dev/null +++ b/libs/bsa_extractor/src/bsa_extractor_en.ts @@ -0,0 +1,59 @@ + + + + + BsaExtractor + + + BSA Extractor + + + + + Offers a dialog during installation of a mod to unpack all its BSAs + + + + + invalid mod name + + + + + BSA extraction doesn't work on mods that have the same name as a non-MO mod.Please remove the mod then reinstall with a different name. + + + + + Extract BSA + + + + + This mod contains at least one BSA. Do you want to unpack it? +(If you don't know about BSAs, just select no) + + + + + Remove extracted archives + + + + + Do you wish to remove BSAs after extraction completed? + + + + + + failed to read %1: %2 + + + + + This archive contains invalid hashes. Some files may be broken. + + + + diff --git a/libs/bsa_extractor/src/bsaextractor.cpp b/libs/bsa_extractor/src/bsaextractor.cpp new file mode 100644 index 0000000..305edca --- /dev/null +++ b/libs/bsa_extractor/src/bsaextractor.cpp @@ -0,0 +1,140 @@ +#include "bsaextractor.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +using namespace MOBase; + + +BsaExtractor::BsaExtractor() + : m_Organizer(nullptr) +{ +} + +bool BsaExtractor::init(MOBase::IOrganizer *moInfo) +{ + m_Organizer = moInfo; + moInfo->modList()->onModInstalled([this](auto* mod) { modInstalledHandler(mod); }); + return true; +} + +QString BsaExtractor::name() const +{ + return "BSA Extractor"; +} + +QString BsaExtractor::localizedName() const +{ + return tr("BSA Extractor"); +} + + +QString BsaExtractor::author() const +{ + return "Tannin & MO2 Team"; +} + +QString BsaExtractor::description() const +{ + return tr("Offers a dialog during installation of a mod to unpack all its BSAs"); +} + +VersionInfo BsaExtractor::version() const +{ + return VersionInfo(1, 5, 0, VersionInfo::RELEASE_FINAL); +} + +QList BsaExtractor::settings() const +{ + return { + PluginSetting("only_alternate_source", "only trigger bsa extraction for alternate game sources", true) + }; +} + + +bool BsaExtractor::extractProgress(QProgressDialog &progress, int percentage, std::string fileName) +{ + progress.setLabelText(fileName.c_str()); + progress.setValue(percentage); + qApp->processEvents(); + return !progress.wasCanceled(); +} + + +void BsaExtractor::modInstalledHandler(IModInterface *mod) +{ + + if (m_Organizer->pluginSetting(name(), "only_alternate_source").toBool() && + !(m_Organizer->modList()->state(mod->name()) & IModList::STATE_ALTERNATE)) { + return; + } + + if (QFileInfo(mod->absolutePath()) == QFileInfo(m_Organizer->managedGame()->dataDirectory().absolutePath())) { + QMessageBox::information(nullptr, tr("invalid mod name"), + tr("BSA extraction doesn't work on mods that have the same name as a non-MO mod." + "Please remove the mod then reinstall with a different name.")); + return; + } + QDir dir(mod->absolutePath()); + + QFileInfoList archives = dir.entryInfoList(QStringList({ "*.bsa", "*.ba2" })); + if (archives.length() != 0 && + (QuestionBoxMemory::query(nullptr, "unpackBSA", tr("Extract BSA"), + tr("This mod contains at least one BSA. Do you want to unpack it?\n" + "(If you don't know about BSAs, just select no)"), + QDialogButtonBox::Yes | QDialogButtonBox::No, QDialogButtonBox::No) == QDialogButtonBox::Yes)) { + + bool removeBSAs = (QuestionBoxMemory::query(nullptr, "removeUnpackedBSA", tr("Remove extracted archives"), + tr("Do you wish to remove BSAs after extraction completed?\n"), + QDialogButtonBox::Yes | QDialogButtonBox::No, QDialogButtonBox::No) == QDialogButtonBox::Yes); + foreach (QFileInfo archiveInfo, archives) { + BSA::Archive archive; + BSA::EErrorCode result = archive.read(archiveInfo.absoluteFilePath().toLocal8Bit().constData(), true); + if ((result != BSA::ERROR_NONE) && (result != BSA::ERROR_INVALIDHASHES)) { + reportError(tr("failed to read %1: %2").arg(archiveInfo.fileName()).arg(result)); + return; + } + + QProgressDialog progress(nullptr); + progress.setMaximum(100); + progress.setValue(0); + progress.show(); + + archive.extractAll(mod->absolutePath().toLocal8Bit().constData(), + [this, &progress](int value, std::string filename) { + return extractProgress(progress, value, filename); + }, + false); + + if (result == BSA::ERROR_INVALIDHASHES) { + reportError(tr("This archive contains invalid hashes. Some files may be broken.")); + } + + archive.close(); + + if (removeBSAs) { + if (!QFile::remove(archiveInfo.absoluteFilePath())) { + qCritical("failed to remove archive %s", archiveInfo.absoluteFilePath().toUtf8().constData()); + } + } + } + } +} + +#if QT_VERSION < QT_VERSION_CHECK(5,0,0) +Q_EXPORT_PLUGIN2(bsaextractor, BsaExtractor) +#endif diff --git a/libs/bsa_extractor/src/bsaextractor.h b/libs/bsa_extractor/src/bsaextractor.h new file mode 100644 index 0000000..09631ff --- /dev/null +++ b/libs/bsa_extractor/src/bsaextractor.h @@ -0,0 +1,34 @@ +#ifndef BSAEXTRACTOR_H +#define BSAEXTRACTOR_H + +#include + +#include + +class BsaExtractor : public QObject, public MOBase::IPlugin +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin) +#if QT_VERSION >= QT_VERSION_CHECK(5,0,0) + Q_PLUGIN_METADATA(IID "org.tannin.BSAExtractor") +#endif + +public: + BsaExtractor(); + + virtual bool init(MOBase::IOrganizer *moInfo) override; + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +private: + void modInstalledHandler(MOBase::IModInterface *mod); + bool extractProgress(QProgressDialog &progress, int percentage, std::string fileName); +private: + MOBase::IOrganizer *m_Organizer; +}; + +#endif // BSAEXTRACTOR_H diff --git a/libs/bsa_extractor/vcpkg.json b/libs/bsa_extractor/vcpkg.json new file mode 100644 index 0000000..9ff119d --- /dev/null +++ b/libs/bsa_extractor/vcpkg.json @@ -0,0 +1,36 @@ +{ + "dependencies": [ + "mo2-dds-header", + "boost-thread", + "boost-interprocess", + "zlib", + "lz4" + ], + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43", + "packages": ["boost*", "boost-*"] + }, + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673", + "packages": ["mo2-*"] + } + ] + } +} diff --git a/libs/bsa_ffi/CMakeLists.txt b/libs/bsa_ffi/CMakeLists.txt new file mode 100644 index 0000000..26da7ff --- /dev/null +++ b/libs/bsa_ffi/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.16) +project(bsa_ffi) + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(CARGO_BUILD_TYPE "debug") + set(CARGO_BUILD_FLAGS "") +else() + set(CARGO_BUILD_TYPE "release") + set(CARGO_BUILD_FLAGS "--release") +endif() + +set(BSA_FFI_DIR ${CMAKE_CURRENT_SOURCE_DIR}) +set(BSA_FFI_LIB ${BSA_FFI_DIR}/target/${CARGO_BUILD_TYPE}/libbsa_ffi.so) + +add_custom_command( + OUTPUT ${BSA_FFI_LIB} + COMMAND cargo build ${CARGO_BUILD_FLAGS} + WORKING_DIRECTORY ${BSA_FFI_DIR} + COMMENT "Building BSA FFI library (Rust)" + DEPENDS + ${BSA_FFI_DIR}/Cargo.toml + ${BSA_FFI_DIR}/src/lib.rs +) + +add_custom_target(bsa_ffi_build DEPENDS ${BSA_FFI_LIB}) + +add_library(bsa_ffi_wrapper INTERFACE) +target_link_libraries(bsa_ffi_wrapper INTERFACE ${BSA_FFI_LIB}) +target_include_directories(bsa_ffi_wrapper INTERFACE ${BSA_FFI_DIR}/include) +add_dependencies(bsa_ffi_wrapper bsa_ffi_build) +add_library(mo2::bsa_ffi ALIAS bsa_ffi_wrapper) + +install(FILES ${BSA_FFI_LIB} DESTINATION lib) +install(DIRECTORY ${BSA_FFI_DIR}/include/ DESTINATION include) diff --git a/libs/bsa_ffi/Cargo.lock b/libs/bsa_ffi/Cargo.lock new file mode 100644 index 0000000..62d98ad --- /dev/null +++ b/libs/bsa_ffi/Cargo.lock @@ -0,0 +1,520 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "ba2" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdb05c5c954898b463887df1145016492deee06e9a778f8af491c7cde14c210" +dependencies = [ + "bitflags", + "bstr", + "directxtex", + "flate2", + "lzzzz", + "memmap2", + "thiserror", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "directxtex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0cea4cfe7ef3eeeae77cbbe6c8eddb7e518b1af877ca788b7a9d4fde45e658" +dependencies = [ + "bitflags", + "cc", + "winresult", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.181" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" + +[[package]] +name = "libz-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "lzzzz" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac94cca0c9c2ac03c63092f1377df5b83e4c35441f9d83a53ca214c58685f7bd" +dependencies = [ + "cc", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mo2_bsa_ffi" +version = "0.1.0" +dependencies = [ + "anyhow", + "ba2", + "byteorder", + "flate2", + "lz4_flex", + "rayon", + "tracing", + "walkdir", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winresult" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe1d4015321f3df114e73ccc695d62c1566b33b0a2d03d553b7dd65dc11f58d" +dependencies = [ + "winresult-types", +] + +[[package]] +name = "winresult-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7314fd4ed99b7cc41081400f5d072919967e7382d358bc9e7d72bdd1955e1e8" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" diff --git a/libs/bsa_ffi/Cargo.toml b/libs/bsa_ffi/Cargo.toml new file mode 100644 index 0000000..4b5ef20 --- /dev/null +++ b/libs/bsa_ffi/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "mo2_bsa_ffi" +version = "0.1.0" +edition = "2021" + +[lib] +name = "bsa_ffi" +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1.0" +ba2 = "3.0.1" +rayon = "1.11" +flate2 = "1.1" +lz4_flex = "0.11" +byteorder = "1.5" +tracing = "0.1" +walkdir = "2.5" diff --git a/libs/bsa_ffi/include/bsa_ffi.h b/libs/bsa_ffi/include/bsa_ffi.h new file mode 100644 index 0000000..32dcee3 --- /dev/null +++ b/libs/bsa_ffi/include/bsa_ffi.h @@ -0,0 +1,54 @@ +#ifndef BSA_FFI_H +#define BSA_FFI_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + char **items; + size_t count; + char *error; +} BsaFfiStringList; + +typedef void (*BsaProgressCallback)(uint32_t done, uint32_t total, + const char *current_path); + +/* Returns list of paths in archive. On error, error is non-null and must be freed with + * bsa_ffi_string_list_free(). */ +BsaFfiStringList bsa_ffi_list_files(const char *archive_path); + +void bsa_ffi_string_list_free(BsaFfiStringList list); + +/* Returns NULL on success, else an allocated error string (free with bsa_ffi_string_free). */ +char *bsa_ffi_extract_all(const char *archive_path, const char *output_dir, + BsaProgressCallback progress_cb, const int *cancel_flag); + +/* game_id uses CLI ids from GameVersion::cli_name(): + * morrowind, oblivion, fo3, fonv, skyrimle, skyrimse, + * fo4-fo76, fo4ng-v7, fo4ng-v8, starfield-v2, starfield-v3 + */ +char *bsa_ffi_pack_dir(const char *input_dir, const char *output_archive, + const char *game_id, BsaProgressCallback progress_cb, + const int *cancel_flag); + +/* include_mode: + * 0 = all files + * 1 = exclude .dds + * 2 = only .dds + */ +char *bsa_ffi_pack_dir_filtered(const char *input_dir, const char *output_archive, + const char *game_id, int include_mode, + BsaProgressCallback progress_cb, + const int *cancel_flag); + +void bsa_ffi_string_free(char *s); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/libs/bsa_ffi/src/archive/ba2_reader.rs b/libs/bsa_ffi/src/archive/ba2_reader.rs new file mode 100644 index 0000000..e6dd984 --- /dev/null +++ b/libs/bsa_ffi/src/archive/ba2_reader.rs @@ -0,0 +1,125 @@ +//! BA2 (Fallout 4/Starfield) archive reading +//! +//! Provides read support for FO4 format BA2 files (Fallout 4, Fallout 76, Starfield). + +use anyhow::{bail, Context, Result}; +use ba2::fo4::{Archive, File as Ba2File, FileWriteOptions}; +use ba2::prelude::*; +use ba2::ByteSlice; +use rayon::prelude::*; +use std::collections::HashSet; +use std::io::Cursor; +use std::path::Path; +use std::sync::atomic::{AtomicUsize, Ordering}; +use tracing::debug; + +/// Entry for a file in a BA2 archive +#[derive(Debug, Clone)] +pub struct Ba2FileEntry { + pub path: String, +} + +/// List all files in a BA2 archive +pub fn list_files(ba2_path: &Path) -> Result> { + let (archive, _options): (Archive, _) = Archive::read(ba2_path) + .with_context(|| format!("Failed to open BA2: {}", ba2_path.display()))?; + + let mut files = Vec::new(); + + for (key, _file) in archive.iter() { + let path = String::from_utf8_lossy(key.name().as_bytes()).to_string(); + + files.push(Ba2FileEntry { path }); + } + + debug!("Listed {} files in BA2 {}", files.len(), ba2_path.display()); + Ok(files) +} + +/// Extract a single file from a BA2 archive +#[allow(dead_code)] +pub fn extract_file(ba2_path: &Path, file_path: &str) -> Result> { + let (archive, options): (Archive, _) = Archive::read(ba2_path) + .with_context(|| format!("Failed to open BA2: {}", ba2_path.display()))?; + + let write_options: FileWriteOptions = options.into(); + + // Normalize path for comparison (BA2 uses forward slashes typically) + let normalized = file_path.replace('\\', "/").to_lowercase(); + let normalized_backslash = file_path.replace('/', "\\").to_lowercase(); + + for (key, file) in archive.iter() { + let current_path = String::from_utf8_lossy(key.name().as_bytes()).to_lowercase(); + + // Try both slash conventions + if current_path == normalized + || current_path == normalized_backslash + || current_path.replace('\\', "/") == normalized + || current_path.replace('/', "\\") == normalized_backslash + { + // Write to memory buffer + let mut buffer = Cursor::new(Vec::new()); + file.write(&mut buffer, &write_options) + .with_context(|| format!("Failed to extract file: {}", file_path))?; + + return Ok(buffer.into_inner()); + } + } + + bail!( + "File not found in BA2: {} (searched for '{}')", + file_path, + normalized + ) +} + +/// Extract multiple files from a BA2 archive in parallel. +/// Opens the archive once, collects matching entries, then decompresses +/// and writes them in parallel using rayon. +/// `wanted` should contain lowercase forward-slash-separated paths. +pub fn extract_files_batch( + ba2_path: &Path, + wanted: &HashSet, + callback: F, +) -> Result +where + F: Fn(&str, Vec) -> Result<()> + Send + Sync, +{ + let (archive, options): (Archive, _) = Archive::read(ba2_path) + .with_context(|| format!("Failed to open BA2: {}", ba2_path.display()))?; + + let write_options: FileWriteOptions = options.into(); + + // Collect matching entries with references + let mut entries: Vec<(String, &Ba2File)> = Vec::new(); + for (key, file) in archive.iter() { + let path = String::from_utf8_lossy(key.name().as_bytes()).to_string(); + let lookup = path.replace('\\', "/").to_lowercase(); + if wanted.contains(&lookup) { + entries.push((path, file)); + } + } + + // Decompress + write in parallel + let extracted = AtomicUsize::new(0); + entries + .par_iter() + .try_for_each(|(path, file)| -> Result<()> { + let mut buffer = Cursor::new(Vec::new()); + file.write(&mut buffer, &write_options) + .with_context(|| format!("Failed to extract file: {}", path))?; + + callback(path, buffer.into_inner())?; + extracted.fetch_add(1, Ordering::Relaxed); + Ok(()) + })?; + + let count = extracted.load(Ordering::Relaxed); + debug!( + "Batch extracted {} of {} wanted files from BA2 {}", + count, + wanted.len(), + ba2_path.display() + ); + Ok(count) +} diff --git a/libs/bsa_ffi/src/archive/ba2_writer.rs b/libs/bsa_ffi/src/archive/ba2_writer.rs new file mode 100644 index 0000000..aa461d5 --- /dev/null +++ b/libs/bsa_ffi/src/archive/ba2_writer.rs @@ -0,0 +1,336 @@ +//! BA2 (Fallout 4/Starfield) archive creation +//! +//! Provides write support for FO4 format BA2 files (Fallout 4, Fallout 76, Starfield). + +use anyhow::{bail, Context, Result}; +use ba2::fo4::{ + Archive, ArchiveKey, ArchiveOptionsBuilder, Chunk, ChunkCompressionOptions, + CompressionFormat as Ba2CrateCompression, CompressionLevel, File as Ba2File, + FileReadOptionsBuilder, Format, Version, +}; +use ba2::prelude::*; +use ba2::{CompressionResult, Copied}; +use rayon::prelude::*; +use std::collections::HashMap; +use std::fs; +use std::io::BufWriter; +use std::path::Path; +use tracing::info; + +/// BA2 archive version +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Ba2Version { + /// Fallout 4 Old Gen, Fallout 76 + V1, + /// Starfield + V2, + /// Starfield + V3, + /// Fallout 4 Next Gen + #[default] + V7, + /// Fallout 4 Next Gen + V8, +} + +impl Ba2Version { + /// Convert to the ba2 crate's Version type + pub fn to_crate_version(self) -> Version { + match self { + Ba2Version::V1 => Version::v1, + Ba2Version::V2 => Version::v2, + Ba2Version::V3 => Version::v3, + Ba2Version::V7 => Version::v7, + Ba2Version::V8 => Version::v8, + } + } +} + +/// Compression format for BA2 archives +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Ba2CompressionFormat { + /// No compression + None, + /// zlib compression (Fallout 4, Fallout 76) + #[default] + Zlib, + /// LZ4 compression (Starfield) + Lz4, +} + +/// Archive format variant +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Ba2Format { + /// General archive (GNRL) - for meshes, scripts, etc. + #[default] + General, + /// DirectX 10 textures (DX10) - for DDS textures + DX10, +} + +/// Builder for creating BA2 archives +pub struct Ba2Builder { + /// Files organized by path -> data + files: HashMap>, + /// Archive format (General or DX10) + format: Ba2Format, + /// Compression format + compression: Ba2CompressionFormat, + /// Whether to include string table + strings: bool, + /// Archive version + version: Ba2Version, +} + +impl Ba2Builder { + pub fn new() -> Self { + Self { + files: HashMap::new(), + format: Ba2Format::General, + compression: Ba2CompressionFormat::Zlib, + strings: true, + version: Ba2Version::default(), + } + } + + /// Create builder with settings detected from BA2 name + #[allow(dead_code)] + pub fn from_name(name: &str) -> Self { + let name_lower = name.to_lowercase(); + + // Texture archives need DX10 format for proper texture headers + let is_texture_archive = { + let filename = name_lower.rsplit(['/', '\\']).next().unwrap_or(&name_lower); + filename.contains(" - textures") + || filename.starts_with("textures") + || (filename.contains("textures") + && !filename.contains(" - main") + && !filename.contains("_main")) + }; + + let format = if is_texture_archive { + Ba2Format::DX10 + } else { + Ba2Format::General + }; + + // Default to zlib compression for FO4 + let compression = Ba2CompressionFormat::Zlib; + + Self { + files: HashMap::new(), + format, + compression, + strings: true, + version: Ba2Version::default(), + } + } + + /// Set archive version + pub fn with_version(mut self, version: Ba2Version) -> Self { + self.version = version; + self + } + + /// Set archive format + pub fn with_format(mut self, format: Ba2Format) -> Self { + self.format = format; + self + } + + /// Set compression format + pub fn with_compression(mut self, compression: Ba2CompressionFormat) -> Self { + self.compression = compression; + self + } + + /// Enable or disable string table + #[allow(dead_code)] + pub fn with_strings(mut self, strings: bool) -> Self { + self.strings = strings; + self + } + + /// Add a file to the archive + pub fn add_file(&mut self, path: &str, data: Vec) { + // Normalize: forward slashes, strip leading slash + let normalized = path.replace('\\', "/"); + let normalized = normalized.trim_start_matches('/').to_string(); + self.files.insert(normalized, data); + } + + /// Get number of files + pub fn file_count(&self) -> usize { + self.files.len() + } + + /// Check if empty + pub fn is_empty(&self) -> bool { + self.files.is_empty() + } + + /// Build and write the BA2 to disk with progress callback + pub fn build_with_progress(self, output_path: &Path, progress: F) -> Result<()> + where + F: Fn(usize, usize, &str) + Send + Sync, + { + if self.is_empty() { + bail!("Cannot create empty BA2 archive"); + } + + let file_count = self.file_count(); + let total_size: u64 = self.files.values().map(|data| data.len() as u64).sum(); + + info!( + "Building BA2: {} ({} files, {} MB, format {:?}, compression {:?})", + output_path.display(), + file_count, + total_size / 1_000_000, + self.format, + self.compression + ); + + // For DX10 (texture) archives, we need special handling + if self.format == Ba2Format::DX10 { + return self.build_dx10_with_progress(output_path, progress); + } + + // Build archive entries in parallel + let entries: Vec<(String, Vec)> = self.files.into_iter().collect(); + let total = entries.len(); + let processed_count = std::sync::atomic::AtomicUsize::new(0); + let compression = self.compression; + + let archive_entries: Result, Ba2File<'static>)>> = entries + .par_iter() + .map(|(path, data)| { + // Create chunk from data + let chunk = Chunk::from_decompressed(data.clone().into_boxed_slice()); + + // Optionally compress the chunk + let chunk = if compression != Ba2CompressionFormat::None { + let options = ChunkCompressionOptions::default(); + match chunk.compress(&options) { + Ok(compressed) => compressed, + Err(_) => chunk, // Fall back to uncompressed if compression fails + } + } else { + chunk + }; + + // Create file from chunk + let file: Ba2File = [chunk].into_iter().collect(); + + // Create key from path + let key: ArchiveKey = path.as_bytes().into(); + + let current = + processed_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; + progress(current, total, path); + + Ok((key, file)) + }) + .collect(); + + let archive_entries = archive_entries?; + + // Build archive from entries + let archive: Archive = archive_entries.into_iter().collect(); + + let options = ArchiveOptionsBuilder::default() + .version(self.version.to_crate_version()) + .strings(self.strings) + .build(); + + // Create parent directory + if let Some(parent) = output_path.parent() { + fs::create_dir_all(parent)?; + } + + // Write archive + let file = fs::File::create(output_path) + .with_context(|| format!("Failed to create BA2: {}", output_path.display()))?; + let mut writer = BufWriter::new(file); + + archive + .write(&mut writer, &options) + .with_context(|| format!("Failed to write BA2: {}", output_path.display()))?; + + info!("Created BA2: {}", output_path.display()); + Ok(()) + } + + /// Build a DX10 (texture) archive with progress callback + fn build_dx10_with_progress(self, output_path: &Path, progress: F) -> Result<()> + where + F: Fn(usize, usize, &str) + Send + Sync, + { + let compress = self.compression != Ba2CompressionFormat::None; + let entries: Vec<(String, Vec)> = self.files.into_iter().collect(); + let total = entries.len(); + let processed_count = std::sync::atomic::AtomicUsize::new(0); + + // Build read options for DX10 format + let read_options = FileReadOptionsBuilder::new() + .format(Format::DX10) + .compression_format(Ba2CrateCompression::Zip) + .compression_level(CompressionLevel::FO4) + .compression_result(if compress { + CompressionResult::Compressed + } else { + CompressionResult::Decompressed + }) + .build(); + + let archive_entries: Result, Ba2File<'static>)>> = entries + .par_iter() + .map(|(path, data)| { + let file = Ba2File::read(Copied(data), &read_options) + .with_context(|| format!("Failed to parse DDS texture: {}", path))?; + + let key: ArchiveKey = path.as_bytes().into(); + + let current = + processed_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; + progress(current, total, path); + + Ok((key, file)) + }) + .collect(); + + let archive_entries = archive_entries?; + let archive: Archive = archive_entries.into_iter().collect(); + + let options = ArchiveOptionsBuilder::default() + .version(self.version.to_crate_version()) + .format(Format::DX10) + .compression_format(Ba2CrateCompression::Zip) + .strings(self.strings) + .build(); + + if let Some(parent) = output_path.parent() { + fs::create_dir_all(parent)?; + } + + let file = fs::File::create(output_path) + .with_context(|| format!("Failed to create BA2: {}", output_path.display()))?; + let mut writer = BufWriter::new(file); + + archive + .write(&mut writer, &options) + .with_context(|| format!("Failed to write BA2: {}", output_path.display()))?; + + info!( + "Created DX10 BA2: {} ({} files)", + output_path.display(), + total + ); + Ok(()) + } +} + +impl Default for Ba2Builder { + fn default() -> Self { + Self::new() + } +} diff --git a/libs/bsa_ffi/src/archive/mod.rs b/libs/bsa_ffi/src/archive/mod.rs new file mode 100644 index 0000000..247789e --- /dev/null +++ b/libs/bsa_ffi/src/archive/mod.rs @@ -0,0 +1,436 @@ +//! BSA/BA2 (Bethesda Archive) handling +//! +//! Provides read/write support for: +//! - TES3 format BSA files (Morrowind) +//! - TES4 format BSA files (Oblivion, FO3, FNV, Skyrim) +//! - FO4 format BA2 files (Fallout 4, Fallout 76, Starfield) + +mod ba2_reader; +mod ba2_writer; +mod reader; +mod tes3_reader; +mod writer; + +pub use reader::{ + extract_file, extract_files_batch as extract_bsa_files_batch, list_files, BsaFileEntry, +}; +pub use writer::BsaBuilder; + +// TES3 (Morrowind) support +pub use tes3_reader::{ + extract_file as extract_tes3_file, extract_files_batch as extract_tes3_files_batch, + list_files as list_tes3_files, +}; + +// BA2 support for Fallout 4/Starfield +pub use ba2_reader::{ + extract_file as extract_ba2_file, extract_files_batch as extract_ba2_files_batch, + list_files as list_ba2_files, +}; +pub use ba2_writer::{Ba2Builder, Ba2CompressionFormat, Ba2Format, Ba2Version}; + +use anyhow::{bail, Result}; +use ba2::tes4::{ArchiveFlags, ArchiveTypes, Version}; +use ba2::{guess_format, FileFormat, Reader}; +use std::collections::HashSet; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; +use tracing::debug; + +/// Archive format type +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ArchiveFormat { + /// TES3 BSA (Morrowind) + Tes3Bsa, + /// TES4 BSA (Oblivion, FO3, FNV, Skyrim) + Bsa, + /// FO4 BA2 (Fallout 4, Fallout 76, Starfield) + Ba2, +} + +/// Detect archive format using ba2 crate's guess_format +pub fn detect_format(path: &Path) -> Option { + // Use ba2's built-in format detection + if let Ok(file) = File::open(path) { + let mut reader = BufReader::new(file); + if let Some(format) = guess_format(&mut reader) { + let result = match format { + FileFormat::TES3 => ArchiveFormat::Tes3Bsa, + FileFormat::TES4 => ArchiveFormat::Bsa, + FileFormat::FO4 => ArchiveFormat::Ba2, + }; + debug!("Detected {:?} format for: {}", result, path.display()); + return Some(result); + } + } + + // Fall back to extension + let ext = path.extension()?.to_str()?.to_lowercase(); + match ext.as_str() { + "bsa" => { + debug!( + "Detected BSA by extension (assuming TES4): {}", + path.display() + ); + Some(ArchiveFormat::Bsa) + } + "ba2" => { + debug!("Detected BA2 by extension: {}", path.display()); + Some(ArchiveFormat::Ba2) + } + _ => None, + } +} + +/// Universal archive file entry +#[derive(Debug, Clone)] +pub struct ArchiveFileEntry { + pub path: String, +} + +/// List files from any Bethesda archive (TES3 BSA, TES4 BSA, or BA2) +pub fn list_archive_files(archive_path: &Path) -> Result> { + match detect_format(archive_path) { + Some(ArchiveFormat::Tes3Bsa) => { + let files = list_tes3_files(archive_path)?; + Ok(files + .into_iter() + .map(|f| ArchiveFileEntry { path: f.path }) + .collect()) + } + Some(ArchiveFormat::Bsa) => { + let files = list_files(archive_path)?; + Ok(files + .into_iter() + .map(|f| ArchiveFileEntry { path: f.path }) + .collect()) + } + Some(ArchiveFormat::Ba2) => { + let files = list_ba2_files(archive_path)?; + Ok(files + .into_iter() + .map(|f| ArchiveFileEntry { path: f.path }) + .collect()) + } + None => bail!("Unknown archive format: {}", archive_path.display()), + } +} + +/// Extract a file from any Bethesda archive (TES3 BSA, TES4 BSA, or BA2) +#[allow(dead_code)] +pub fn extract_archive_file(archive_path: &Path, file_path: &str) -> Result> { + let format = detect_format(archive_path); + debug!( + "extract_archive_file: archive={}, file={}, format={:?}", + archive_path.display(), + file_path, + format + ); + match format { + Some(ArchiveFormat::Tes3Bsa) => extract_tes3_file(archive_path, file_path), + Some(ArchiveFormat::Bsa) => extract_file(archive_path, file_path), + Some(ArchiveFormat::Ba2) => extract_ba2_file(archive_path, file_path), + None => bail!("Unknown archive format: {}", archive_path.display()), + } +} + +/// Extract multiple files from any Bethesda archive in a single pass. +/// Opens the archive once and calls the callback for each extracted file. +/// `wanted_files` should contain the original paths (as returned by list_archive_files). +/// Returns the number of files successfully extracted. +pub fn extract_archive_files_batch( + archive_path: &Path, + wanted_files: &[String], + callback: F, +) -> Result +where + F: Fn(&str, Vec) -> Result<()> + Send + Sync, +{ + let format = detect_format(archive_path); + match format { + Some(ArchiveFormat::Tes3Bsa) => { + let wanted: HashSet = wanted_files.iter().map(|p| p.to_lowercase()).collect(); + extract_tes3_files_batch(archive_path, &wanted, callback) + } + Some(ArchiveFormat::Bsa) => { + // BSA uses backslash-separated paths + let wanted: HashSet = wanted_files + .iter() + .map(|p| p.replace('/', "\\").to_lowercase()) + .collect(); + extract_bsa_files_batch(archive_path, &wanted, callback) + } + Some(ArchiveFormat::Ba2) => { + // BA2 uses forward-slash paths + let wanted: HashSet = wanted_files + .iter() + .map(|p| p.replace('\\', "/").to_lowercase()) + .collect(); + extract_ba2_files_batch(archive_path, &wanted, callback) + } + None => bail!("Unknown archive format: {}", archive_path.display()), + } +} + +/// Game version for archive creation +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum GameVersion { + /// TES3 BSA (Morrowind) - no compression + Morrowind, + /// TES4 v103 (Oblivion) - no compression + Oblivion, + /// TES4 v104 (Fallout 3) - zlib compression + Fallout3, + /// TES4 v104 (Fallout: New Vegas) - zlib compression + FalloutNewVegas, + /// TES4 v104 (Skyrim LE) - zlib compression + SkyrimLE, + /// TES4 v105 (Skyrim SE) - zlib compression + SkyrimSE, + /// BA2 v1 (Fallout 4 / Fallout 76) - zlib compression + #[default] + Fallout4Fo76, + /// BA2 v7 (Fallout 4 Next Gen) - zlib compression + Fallout4NGv7, + /// BA2 v8 (Fallout 4 Next Gen) - zlib compression + Fallout4NGv8, + /// BA2 v2 (Starfield) - LZ4 compression + StarfieldV2, + /// BA2 v3 (Starfield) - LZ4 compression + StarfieldV3, +} + +impl GameVersion { + /// Get display name for this game version + pub fn display_name(&self) -> &'static str { + match self { + GameVersion::Morrowind => "Morrowind (BSA)", + GameVersion::Oblivion => "Oblivion (BSA v103)", + GameVersion::Fallout3 => "Fallout 3 (BSA v104)", + GameVersion::FalloutNewVegas => "Fallout: New Vegas (BSA v104)", + GameVersion::SkyrimLE => "Skyrim LE (BSA v104)", + GameVersion::SkyrimSE => "Skyrim SE (BSA v105)", + GameVersion::Fallout4Fo76 => "Fallout 4 / Fallout 76 (BA2 v1)", + GameVersion::Fallout4NGv7 => "Fallout 4 Next Gen (BA2 v7)", + GameVersion::Fallout4NGv8 => "Fallout 4 Next Gen (BA2 v8)", + GameVersion::StarfieldV2 => "Starfield (BA2 v2)", + GameVersion::StarfieldV3 => "Starfield (BA2 v3)", + } + } + + /// Check if this game uses BA2 format + pub fn is_ba2(&self) -> bool { + matches!( + self, + GameVersion::Fallout4Fo76 + | GameVersion::Fallout4NGv7 + | GameVersion::Fallout4NGv8 + | GameVersion::StarfieldV2 + | GameVersion::StarfieldV3 + ) + } + + /// Check if this game uses TES3 format (Morrowind) + pub fn is_tes3(&self) -> bool { + matches!(self, GameVersion::Morrowind) + } + + /// Check if compression is supported for this game + pub fn supports_compression(&self) -> bool { + !matches!(self, GameVersion::Morrowind | GameVersion::Oblivion) + } + + /// Get BSA version for TES4 format games + pub fn bsa_version(&self) -> Option { + match self { + GameVersion::Oblivion => Some(Version::v103), + GameVersion::Fallout3 | GameVersion::FalloutNewVegas | GameVersion::SkyrimLE => { + Some(Version::v104) + } + GameVersion::SkyrimSE => Some(Version::v105), + _ => None, + } + } + + /// Get BA2 version for FO4/Starfield format games + pub fn ba2_version(&self) -> Option { + match self { + GameVersion::Fallout4Fo76 => Some(Ba2Version::V1), + GameVersion::Fallout4NGv7 => Some(Ba2Version::V7), + GameVersion::Fallout4NGv8 => Some(Ba2Version::V8), + GameVersion::StarfieldV2 => Some(Ba2Version::V2), + GameVersion::StarfieldV3 => Some(Ba2Version::V3), + _ => None, + } + } + + /// Get BA2 compression format for this game + pub fn ba2_compression(&self) -> Ba2CompressionFormat { + match self { + GameVersion::StarfieldV2 | GameVersion::StarfieldV3 => Ba2CompressionFormat::Lz4, + _ => Ba2CompressionFormat::Zlib, + } + } + + /// Get all game versions + pub fn all() -> &'static [GameVersion] { + &[ + GameVersion::Morrowind, + GameVersion::Oblivion, + GameVersion::Fallout3, + GameVersion::FalloutNewVegas, + GameVersion::SkyrimLE, + GameVersion::SkyrimSE, + GameVersion::Fallout4Fo76, + GameVersion::Fallout4NGv7, + GameVersion::Fallout4NGv8, + GameVersion::StarfieldV2, + GameVersion::StarfieldV3, + ] + } + + /// Convert index to game version + pub fn from_index(index: i32) -> GameVersion { + match index { + 0 => GameVersion::Morrowind, + 1 => GameVersion::Oblivion, + 2 => GameVersion::Fallout3, + 3 => GameVersion::FalloutNewVegas, + 4 => GameVersion::SkyrimLE, + 5 => GameVersion::SkyrimSE, + 6 => GameVersion::Fallout4Fo76, + 7 => GameVersion::Fallout4NGv7, + 8 => GameVersion::Fallout4NGv8, + 9 => GameVersion::StarfieldV2, + 10 => GameVersion::StarfieldV3, + _ => GameVersion::Fallout4Fo76, + } + } + + /// Convert game version to index + pub fn index(self) -> i32 { + match self { + GameVersion::Morrowind => 0, + GameVersion::Oblivion => 1, + GameVersion::Fallout3 => 2, + GameVersion::FalloutNewVegas => 3, + GameVersion::SkyrimLE => 4, + GameVersion::SkyrimSE => 5, + GameVersion::Fallout4Fo76 => 6, + GameVersion::Fallout4NGv7 => 7, + GameVersion::Fallout4NGv8 => 8, + GameVersion::StarfieldV2 => 9, + GameVersion::StarfieldV3 => 10, + } + } + + /// Short CLI-friendly name + pub fn cli_name(&self) -> &'static str { + match self { + GameVersion::Morrowind => "morrowind", + GameVersion::Oblivion => "oblivion", + GameVersion::Fallout3 => "fo3", + GameVersion::FalloutNewVegas => "fonv", + GameVersion::SkyrimLE => "skyrimle", + GameVersion::SkyrimSE => "skyrimse", + GameVersion::Fallout4Fo76 => "fo4-fo76", + GameVersion::Fallout4NGv7 => "fo4ng-v7", + GameVersion::Fallout4NGv8 => "fo4ng-v8", + GameVersion::StarfieldV2 => "starfield-v2", + GameVersion::StarfieldV3 => "starfield-v3", + } + } + + /// Parse from CLI name (case-insensitive) + pub fn from_cli_name(name: &str) -> Option { + let lower = name.to_lowercase(); + GameVersion::all() + .iter() + .find(|v| v.cli_name() == lower) + .copied() + } +} + +/// Detect game version from archive format +pub fn detect_game_version(archive_path: &Path) -> Option { + match detect_format(archive_path) { + Some(ArchiveFormat::Tes3Bsa) => Some(GameVersion::Morrowind), + Some(ArchiveFormat::Ba2) => Some(GameVersion::Fallout4Fo76), // Default to FO4/FO76 + Some(ArchiveFormat::Bsa) => { + // Try to detect version from BSA header + let result: Result<(ba2::tes4::Archive, ba2::tes4::ArchiveOptions), _> = + ba2::tes4::Archive::read(archive_path); + if let Ok((_, options)) = result { + match options.version() { + Version::v103 => Some(GameVersion::Oblivion), + Version::v104 => Some(GameVersion::Fallout3), // Default for v104 + Version::v105 => Some(GameVersion::SkyrimSE), + } + } else { + Some(GameVersion::Fallout3) // Default + } + } + None => None, + } +} + +/// Default flags for FO3/FNV BSAs +pub fn default_flags_fo3() -> ArchiveFlags { + ArchiveFlags::DIRECTORY_STRINGS + | ArchiveFlags::FILE_STRINGS + | ArchiveFlags::COMPRESSED + | ArchiveFlags::RETAIN_DIRECTORY_NAMES + | ArchiveFlags::RETAIN_FILE_NAMES + | ArchiveFlags::RETAIN_FILE_NAME_OFFSETS +} + +/// Default flags for Oblivion BSAs (no compression) +#[allow(dead_code)] +pub fn default_flags_oblivion() -> ArchiveFlags { + ArchiveFlags::DIRECTORY_STRINGS | ArchiveFlags::FILE_STRINGS +} + +/// Detect archive types from BSA name +#[allow(dead_code)] +pub fn detect_types(name: &str) -> ArchiveTypes { + let name_lower = name.to_lowercase(); + + if name_lower.contains("meshes") { + ArchiveTypes::MESHES + } else if name_lower.contains("textures") { + ArchiveTypes::TEXTURES + } else if name_lower.contains("menuvoices") { + ArchiveTypes::MENUS | ArchiveTypes::VOICES + } else if name_lower.contains("voices") { + ArchiveTypes::VOICES + } else if name_lower.contains("sound") { + ArchiveTypes::SOUNDS + } else { + ArchiveTypes::MISC + } +} + +/// Detect BSA version from archive name +#[allow(dead_code)] +pub fn detect_version(name: &str) -> Version { + let name_lower = name.to_lowercase(); + + // Oblivion uses v103 + if name_lower.contains("oblivion") + || name_lower.contains("shiveringisles") + || name_lower.contains("dlcshiveringisles") + || name_lower.contains("dlcbattlehorn") + || name_lower.contains("dlcfrostcrag") + || name_lower.contains("dlchorse") + || name_lower.contains("dlcorrery") + || name_lower.contains("dlcthievesden") + || name_lower.contains("dlcvilelair") + || name_lower.contains("knights") + { + Version::v103 + } else { + // Default to FO3/FNV + Version::v104 + } +} diff --git a/libs/bsa_ffi/src/archive/reader.rs b/libs/bsa_ffi/src/archive/reader.rs new file mode 100644 index 0000000..21b291c --- /dev/null +++ b/libs/bsa_ffi/src/archive/reader.rs @@ -0,0 +1,154 @@ +//! BSA reading with parallel extraction + +use anyhow::{bail, Context, Result}; +use ba2::tes4::{Archive, File as BsaFile, FileCompressionOptions}; +use ba2::{ByteSlice, Reader}; +use rayon::prelude::*; +use std::collections::HashSet; +use std::path::Path; +use std::sync::atomic::{AtomicUsize, Ordering}; +use tracing::debug; + +/// Entry for a file in a BSA archive +#[derive(Debug, Clone)] +pub struct BsaFileEntry { + pub path: String, +} + +/// List all files in a BSA archive +pub fn list_files(bsa_path: &Path) -> Result> { + let (archive, _): (Archive, _) = Archive::read(bsa_path) + .with_context(|| format!("Failed to open BSA: {}", bsa_path.display()))?; + + let mut files = Vec::new(); + + for (dir_key, folder) in archive.iter() { + let dir_name = String::from_utf8_lossy(dir_key.name().as_bytes()); + + for (file_key, _file) in folder.iter() { + let file_name = String::from_utf8_lossy(file_key.name().as_bytes()); + + // Build full path with backslash (BSA convention) + let full_path = if dir_name.is_empty() || dir_name == "." { + file_name.to_string() + } else { + format!("{}\\{}", dir_name, file_name) + }; + + files.push(BsaFileEntry { path: full_path }); + } + } + + debug!("Listed {} files in BSA {}", files.len(), bsa_path.display()); + Ok(files) +} + +/// Extract a single file from a BSA archive +#[allow(dead_code)] +pub fn extract_file(bsa_path: &Path, file_path: &str) -> Result> { + let (archive, options): (Archive, _) = Archive::read(bsa_path) + .with_context(|| format!("Failed to open BSA: {}", bsa_path.display()))?; + + // Convert archive options to compression options (includes version info) + let compression_options: FileCompressionOptions = (&options).into(); + + // Normalize to backslashes and split + let normalized = file_path.replace('/', "\\"); + let (dir_name, file_name) = if let Some(idx) = normalized.rfind('\\') { + (&normalized[..idx], &normalized[idx + 1..]) + } else { + ("", normalized.as_str()) + }; + + // Search case-insensitively + for (dir_key, folder) in archive.iter() { + let current_dir = String::from_utf8_lossy(dir_key.name().as_bytes()); + + if current_dir.eq_ignore_ascii_case(dir_name) { + for (file_key, file) in folder.iter() { + let current_file = String::from_utf8_lossy(file_key.name().as_bytes()); + + if current_file.eq_ignore_ascii_case(file_name) { + // Extract with decompression if needed (uses version from archive options) + let data = if file.is_decompressed() { + file.as_bytes().to_vec() + } else { + file.decompress(&compression_options)?.as_bytes().to_vec() + }; + return Ok(data); + } + } + } + } + + bail!( + "File not found in BSA: {} (dir='{}', file='{}')", + file_path, + dir_name, + file_name + ) +} + +/// Extract multiple files from a BSA archive in a single parallel pass. +/// Opens the archive once, collects matching entries, then decompresses +/// and writes them in parallel using rayon. +/// `wanted` should contain lowercase backslash-separated paths. +pub fn extract_files_batch( + bsa_path: &Path, + wanted: &HashSet, + callback: F, +) -> Result +where + F: Fn(&str, Vec) -> Result<()> + Send + Sync, +{ + let (archive, options): (Archive, _) = Archive::read(bsa_path) + .with_context(|| format!("Failed to open BSA: {}", bsa_path.display()))?; + + let compression_options: FileCompressionOptions = (&options).into(); + + // Collect matching entries with references to file data + let mut entries: Vec<(String, &BsaFile)> = Vec::new(); + for (dir_key, folder) in archive.iter() { + let dir_name = String::from_utf8_lossy(dir_key.name().as_bytes()); + + for (file_key, file) in folder.iter() { + let file_name = String::from_utf8_lossy(file_key.name().as_bytes()); + + let full_path = if dir_name.is_empty() || dir_name == "." { + file_name.to_string() + } else { + format!("{}\\{}", dir_name, file_name) + }; + + let lookup = full_path.to_lowercase(); + if wanted.contains(&lookup) { + entries.push((full_path, file)); + } + } + } + + // Decompress + write in parallel + let extracted = AtomicUsize::new(0); + entries + .par_iter() + .try_for_each(|(path, file)| -> Result<()> { + let data = if file.is_decompressed() { + file.as_bytes().to_vec() + } else { + file.decompress(&compression_options)?.as_bytes().to_vec() + }; + + callback(path, data)?; + extracted.fetch_add(1, Ordering::Relaxed); + Ok(()) + })?; + + let count = extracted.load(Ordering::Relaxed); + debug!( + "Batch extracted {} of {} wanted files from BSA {}", + count, + wanted.len(), + bsa_path.display() + ); + Ok(count) +} diff --git a/libs/bsa_ffi/src/archive/tes3_reader.rs b/libs/bsa_ffi/src/archive/tes3_reader.rs new file mode 100644 index 0000000..c03f155 --- /dev/null +++ b/libs/bsa_ffi/src/archive/tes3_reader.rs @@ -0,0 +1,104 @@ +//! TES3 (Morrowind) BSA reading + +use anyhow::{bail, Context, Result}; +use ba2::tes3::{Archive, File as Tes3File}; +use ba2::{ByteSlice, Reader}; +use rayon::prelude::*; +use std::collections::HashSet; +use std::path::Path; +use std::sync::atomic::{AtomicUsize, Ordering}; +use tracing::debug; + +use super::BsaFileEntry; + +/// List all files in a TES3 (Morrowind) BSA archive +pub fn list_files(bsa_path: &Path) -> Result> { + let archive: Archive = Archive::read(bsa_path) + .with_context(|| format!("Failed to open TES3 BSA: {}", bsa_path.display()))?; + + let mut files = Vec::new(); + + for (key, _file) in archive.iter() { + let path = String::from_utf8_lossy(key.name().as_bytes()).to_string(); + + files.push(BsaFileEntry { path }); + } + + debug!( + "Listed {} files in TES3 BSA {}", + files.len(), + bsa_path.display() + ); + Ok(files) +} + +/// Extract a single file from a TES3 (Morrowind) BSA archive +#[allow(dead_code)] +pub fn extract_file(bsa_path: &Path, file_path: &str) -> Result> { + let archive: Archive = Archive::read(bsa_path) + .with_context(|| format!("Failed to open TES3 BSA: {}", bsa_path.display()))?; + + // Normalize path separators + let normalized = file_path.replace('/', "\\"); + + // Search case-insensitively + for (key, file) in archive.iter() { + let current_path = String::from_utf8_lossy(key.name().as_bytes()); + + if current_path.eq_ignore_ascii_case(&normalized) { + // TES3 BSAs are uncompressed, so just return the raw bytes + return Ok(file.as_bytes().to_vec()); + } + } + + bail!( + "File not found in TES3 BSA: {} (looking for '{}')", + bsa_path.display(), + file_path + ) +} + +/// Extract multiple files from a TES3 BSA archive in parallel. +/// Opens the archive once, collects matching entries, then writes +/// them in parallel using rayon. +/// `wanted` should contain lowercase backslash-separated paths. +pub fn extract_files_batch( + bsa_path: &Path, + wanted: &HashSet, + callback: F, +) -> Result +where + F: Fn(&str, Vec) -> Result<()> + Send + Sync, +{ + let archive: Archive = Archive::read(bsa_path) + .with_context(|| format!("Failed to open TES3 BSA: {}", bsa_path.display()))?; + + // Collect matching entries + let mut entries: Vec<(String, &Tes3File)> = Vec::new(); + for (key, file) in archive.iter() { + let path = String::from_utf8_lossy(key.name().as_bytes()).to_string(); + let lookup = path.to_lowercase(); + if wanted.contains(&lookup) { + entries.push((path, file)); + } + } + + // Write in parallel + let extracted = AtomicUsize::new(0); + entries + .par_iter() + .try_for_each(|(path, file)| -> Result<()> { + callback(path, file.as_bytes().to_vec())?; + extracted.fetch_add(1, Ordering::Relaxed); + Ok(()) + })?; + + let count = extracted.load(Ordering::Relaxed); + debug!( + "Batch extracted {} of {} wanted files from TES3 BSA {}", + count, + wanted.len(), + bsa_path.display() + ); + Ok(count) +} diff --git a/libs/bsa_ffi/src/archive/writer.rs b/libs/bsa_ffi/src/archive/writer.rs new file mode 100644 index 0000000..112619f --- /dev/null +++ b/libs/bsa_ffi/src/archive/writer.rs @@ -0,0 +1,256 @@ +//! BSA archive creation + +use anyhow::{bail, Context, Result}; +use ba2::tes4::{ + Archive, ArchiveFlags, ArchiveKey, ArchiveOptions, ArchiveTypes, Directory, DirectoryKey, + File as BsaFile, FileCompressionOptions, Version, +}; +use ba2::CompressableFrom; +use rayon::prelude::*; +use std::collections::HashMap; +use std::fs; +use std::io::BufWriter; +use std::path::Path; +use tracing::info; + +use super::{default_flags_fo3, default_flags_oblivion, detect_types, detect_version}; + +/// Helper struct to hold file data with lifetime for BSA creation +struct FileEntry { + dir_path: String, + file_name: String, + data: Vec, +} + +impl FileEntry { + /// Create a BSA file, optionally compressing it + fn as_bsa_file(&self, version: Version, should_compress: bool) -> Result> { + // Create an uncompressed file from our raw data + let uncompressed = BsaFile::from_decompressed(self.data.clone().into_boxed_slice()); + + if should_compress { + // Compress the file using ba2's compress method + let compression_options = FileCompressionOptions::builder().version(version).build(); + + uncompressed + .compress(&compression_options) + .with_context(|| { + format!("Failed to compress: {}/{}", self.dir_path, self.file_name) + }) + } else { + Ok(uncompressed) + } + } +} + +/// Builder for creating BSA archives +pub struct BsaBuilder { + /// Files organized by directory -> filename -> data + files: HashMap>>, + flags: ArchiveFlags, + types: ArchiveTypes, + version: Version, +} + +impl BsaBuilder { + pub fn new() -> Self { + Self { + files: HashMap::new(), + flags: default_flags_fo3(), + types: ArchiveTypes::empty(), + version: Version::v104, + } + } + + /// Create builder with settings detected from BSA name + #[allow(dead_code)] + pub fn from_name(name: &str) -> Self { + let version = detect_version(name); + let types = detect_types(name); + let flags = if version == Version::v103 { + default_flags_oblivion() + } else { + default_flags_fo3() + }; + + Self { + files: HashMap::new(), + flags, + types, + version, + } + } + + /// Set archive flags + #[allow(dead_code)] + pub fn with_flags(mut self, flags: ArchiveFlags) -> Self { + self.flags = flags; + self + } + + /// Set archive types + #[allow(dead_code)] + pub fn with_types(mut self, types: ArchiveTypes) -> Self { + self.types = types; + self + } + + /// Set BSA version + pub fn with_version(mut self, version: Version) -> Self { + self.version = version; + self + } + + /// Enable or disable compression + pub fn with_compression(mut self, compress: bool) -> Self { + if compress { + self.flags |= ArchiveFlags::COMPRESSED; + } else { + self.flags &= !ArchiveFlags::COMPRESSED; + } + self + } + + /// Add a file to the archive + pub fn add_file(&mut self, path: &str, data: Vec) { + // Normalize: forward slashes, strip leading slash + let normalized = path.replace('\\', "/"); + let normalized = normalized.trim_start_matches('/'); + + let (dir_path, file_name) = if let Some(idx) = normalized.rfind('/') { + ( + normalized[..idx].to_string(), + normalized[idx + 1..].to_string(), + ) + } else { + (".".to_string(), normalized.to_string()) + }; + + self.files + .entry(dir_path) + .or_default() + .insert(file_name, data); + } + + /// Get number of files + pub fn file_count(&self) -> usize { + self.files.values().map(|d| d.len()).sum() + } + + /// Check if empty + pub fn is_empty(&self) -> bool { + self.file_count() == 0 + } + + /// Build and write the BSA to disk with progress callback + pub fn build_with_progress(self, output_path: &Path, progress: F) -> Result<()> + where + F: Fn(usize, usize, &str) + Send + Sync, + { + if self.is_empty() { + bail!("Cannot create empty BSA archive"); + } + + let file_count = self.file_count(); + let total_size: u64 = self + .files + .values() + .flat_map(|files| files.values()) + .map(|data| data.len() as u64) + .sum(); + + info!( + "Building BSA: {} ({} files, {} MB, version {:?}, flags {:?})", + output_path.display(), + file_count, + total_size / 1_000_000, + self.version, + self.flags + ); + + // Check if we should compress files + let should_compress = self.flags.contains(ArchiveFlags::COMPRESSED); + + // Flatten to FileEntry structs that own their data + let entries: Vec = self + .files + .into_iter() + .flat_map(|(dir_path, files)| { + files.into_iter().map(move |(file_name, data)| FileEntry { + dir_path: dir_path.clone(), + file_name, + data, + }) + }) + .collect(); + + let total = entries.len(); + let processed_count = std::sync::atomic::AtomicUsize::new(0); + + // Process files in parallel - create and compress BsaFile entries + let version = self.version; + let processed: Result> = entries + .par_iter() + .map(|entry| { + let file = entry.as_bsa_file(version, should_compress)?; + let current = + processed_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; + progress( + current, + total, + &format!("{}/{}", entry.dir_path, entry.file_name), + ); + Ok((entry.dir_path.clone(), entry.file_name.clone(), file)) + }) + .collect(); + + let processed = processed?; + + // Build archive + let mut archive = Archive::new(); + for (dir_path, file_name, file) in processed { + let archive_key = ArchiveKey::from(dir_path.as_bytes()); + let directory_key = DirectoryKey::from(file_name.as_bytes()); + + match archive.get_mut(&archive_key) { + Some(directory) => { + directory.insert(directory_key, file); + } + None => { + let mut directory = Directory::default(); + directory.insert(directory_key, file); + archive.insert(archive_key, directory); + } + } + } + + let options = ArchiveOptions::builder() + .version(self.version) + .flags(self.flags) + .types(self.types) + .build(); + + // Create parent directory + if let Some(parent) = output_path.parent() { + fs::create_dir_all(parent)?; + } + + // Write archive + let file = fs::File::create(output_path) + .with_context(|| format!("Failed to create BSA: {}", output_path.display()))?; + let mut writer = BufWriter::new(file); + + archive + .write(&mut writer, &options) + .with_context(|| format!("Failed to write BSA: {}", output_path.display()))?; + + info!("Created BSA: {}", output_path.display()); + Ok(()) + } +} + +impl Default for BsaBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/libs/bsa_ffi/src/lib.rs b/libs/bsa_ffi/src/lib.rs new file mode 100644 index 0000000..2cb2f21 --- /dev/null +++ b/libs/bsa_ffi/src/lib.rs @@ -0,0 +1,325 @@ +mod archive; + +use std::ffi::{c_char, c_int, CStr, CString}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::ptr; + +use archive::{ + extract_archive_files_batch, list_archive_files, Ba2Builder, Ba2Format, BsaBuilder, + GameVersion, +}; +use walkdir::WalkDir; + +#[repr(C)] +pub struct BsaFfiStringList { + pub items: *mut *mut c_char, + pub count: usize, + pub error: *mut c_char, +} + +pub type BsaProgressCallback = + Option; + +fn to_cstring(s: &str) -> *mut c_char { + CString::new(s).unwrap_or_default().into_raw() +} + +fn error_list(msg: &str) -> BsaFfiStringList { + BsaFfiStringList { + items: ptr::null_mut(), + count: 0, + error: to_cstring(msg), + } +} + +unsafe fn from_cstr<'a>(p: *const c_char) -> Result<&'a str, &'static str> { + if p.is_null() { + return Err("null pointer"); + } + + CStr::from_ptr(p) + .to_str() + .map_err(|_| "invalid UTF-8 string") +} + +fn call_progress(progress_cb: BsaProgressCallback, done: usize, total: usize, path: &str) { + if let Some(cb) = progress_cb { + if let Ok(c_path) = CString::new(path) { + unsafe { + cb(done as u32, total as u32, c_path.as_ptr()); + } + } + } +} + +fn path_to_rel(root: &Path, child: &Path) -> anyhow::Result { + let rel = child.strip_prefix(root)?; + Ok(rel.to_string_lossy().replace('\\', "/")) +} + +fn include_file_for_mode(rel: &str, include_mode: i32) -> bool { + let is_dds = rel.to_lowercase().ends_with(".dds"); + match include_mode { + 1 => !is_dds, + 2 => is_dds, + _ => true, + } +} + +#[no_mangle] +pub unsafe extern "C" fn bsa_ffi_list_files(archive_path: *const c_char) -> BsaFfiStringList { + let archive_path = match from_cstr(archive_path) { + Ok(v) => v, + Err(e) => return error_list(e), + }; + + let entries = match list_archive_files(Path::new(archive_path)) { + Ok(v) => v, + Err(e) => return error_list(&e.to_string()), + }; + + let mut items: Vec<*mut c_char> = entries.into_iter().map(|e| to_cstring(&e.path)).collect(); + let result = BsaFfiStringList { + items: items.as_mut_ptr(), + count: items.len(), + error: ptr::null_mut(), + }; + std::mem::forget(items); + result +} + +#[no_mangle] +pub unsafe extern "C" fn bsa_ffi_string_list_free(list: BsaFfiStringList) { + if !list.items.is_null() { + let items = Vec::from_raw_parts(list.items, list.count, list.count); + for p in items { + if !p.is_null() { + let _ = CString::from_raw(p); + } + } + } + + if !list.error.is_null() { + let _ = CString::from_raw(list.error); + } +} + +#[no_mangle] +pub unsafe extern "C" fn bsa_ffi_string_free(s: *mut c_char) { + if !s.is_null() { + let _ = CString::from_raw(s); + } +} + +#[no_mangle] +pub unsafe extern "C" fn bsa_ffi_extract_all( + archive_path: *const c_char, + output_dir: *const c_char, + progress_cb: BsaProgressCallback, + cancel_flag: *const c_int, +) -> *mut c_char { + let archive_path = match from_cstr(archive_path) { + Ok(v) => v, + Err(e) => return to_cstring(e), + }; + let output_dir = match from_cstr(output_dir) { + Ok(v) => v, + Err(e) => return to_cstring(e), + }; + + let archive_path = PathBuf::from(archive_path); + let output_dir = PathBuf::from(output_dir); + + if let Err(e) = fs::create_dir_all(&output_dir) { + return to_cstring(&format!("failed to create output directory: {e}")); + } + + let entries = match list_archive_files(&archive_path) { + Ok(v) => v, + Err(e) => return to_cstring(&e.to_string()), + }; + + let total = entries.len(); + let wanted_files: Vec = entries.iter().map(|e| e.path.clone()).collect(); + let progress_count = std::sync::atomic::AtomicUsize::new(0); + let cancel_addr = cancel_flag as usize; + + let res = extract_archive_files_batch(&archive_path, &wanted_files, |path, data| { + let cancel_ptr = cancel_addr as *const c_int; + if !cancel_ptr.is_null() { + let cancelled = unsafe { *cancel_ptr } != 0; + if cancelled { + anyhow::bail!("cancelled"); + } + } + + let out_path = output_dir.join(path.replace('\\', "/")); + if let Some(parent) = out_path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&out_path, &data)?; + + let done = progress_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; + call_progress(progress_cb, done, total, path); + Ok(()) + }); + + match res { + Ok(_) => ptr::null_mut(), + Err(e) => to_cstring(&e.to_string()), + } +} + +#[no_mangle] +pub unsafe extern "C" fn bsa_ffi_pack_dir( + input_dir: *const c_char, + output_archive: *const c_char, + game_id: *const c_char, + progress_cb: BsaProgressCallback, + cancel_flag: *const c_int, +) -> *mut c_char { + bsa_ffi_pack_dir_filtered( + input_dir, + output_archive, + game_id, + 0, + progress_cb, + cancel_flag, + ) +} + +#[no_mangle] +pub unsafe extern "C" fn bsa_ffi_pack_dir_filtered( + input_dir: *const c_char, + output_archive: *const c_char, + game_id: *const c_char, + include_mode: c_int, + progress_cb: BsaProgressCallback, + cancel_flag: *const c_int, +) -> *mut c_char { + let input_dir = match from_cstr(input_dir) { + Ok(v) => v, + Err(e) => return to_cstring(e), + }; + let output_archive = match from_cstr(output_archive) { + Ok(v) => v, + Err(e) => return to_cstring(e), + }; + let game_id = match from_cstr(game_id) { + Ok(v) => v, + Err(e) => return to_cstring(e), + }; + + let game = match GameVersion::from_cli_name(game_id) { + Some(v) => v, + None => { + let valid = GameVersion::all() + .iter() + .map(GameVersion::cli_name) + .collect::>() + .join(", "); + return to_cstring(&format!("unknown game_id '{game_id}', valid: {valid}")); + } + }; + + let input_dir = PathBuf::from(input_dir); + let output_archive = PathBuf::from(output_archive); + + let mut files: Vec<(String, Vec)> = Vec::new(); + for entry in WalkDir::new(&input_dir).into_iter().filter_map(|e| e.ok()) { + if !entry.file_type().is_file() { + continue; + } + + if !cancel_flag.is_null() { + let cancelled = unsafe { *cancel_flag } != 0; + if cancelled { + return to_cstring("cancelled"); + } + } + + let rel = match path_to_rel(&input_dir, entry.path()) { + Ok(v) => v, + Err(e) => return to_cstring(&format!("path error: {e}")), + }; + + if !include_file_for_mode(&rel, include_mode) { + continue; + } + + let data = match fs::read(entry.path()) { + Ok(v) => v, + Err(e) => return to_cstring(&format!("read error: {e}")), + }; + + files.push((rel, data)); + } + + if files.is_empty() { + return to_cstring("no files found in input_dir"); + } + + let total = files.len(); + + if game.is_ba2() { + let ba2_version = game.ba2_version().unwrap_or_default(); + let compression = game.ba2_compression(); + + let format = if output_archive + .file_name() + .map(|n| n.to_string_lossy().to_lowercase().contains("textures")) + .unwrap_or(false) + { + Ba2Format::DX10 + } else { + Ba2Format::General + }; + + let mut builder = Ba2Builder::new() + .with_version(ba2_version) + .with_compression(compression) + .with_format(format); + + for (idx, (rel, data)) in files.into_iter().enumerate() { + if !cancel_flag.is_null() { + let cancelled = unsafe { *cancel_flag } != 0; + if cancelled { + return to_cstring("cancelled"); + } + } + builder.add_file(&rel, data); + call_progress(progress_cb, idx + 1, total, &rel); + } + + match builder.build_with_progress(&output_archive, |_, _, _| {}) { + Ok(_) => ptr::null_mut(), + Err(e) => to_cstring(&e.to_string()), + } + } else { + let version = match game.bsa_version() { + Some(v) => v, + None => return to_cstring("TES3 BSA writing is not supported yet"), + }; + + let compress = game.supports_compression(); + + let mut builder = BsaBuilder::new().with_version(version).with_compression(compress); + + for (idx, (rel, data)) in files.into_iter().enumerate() { + if !cancel_flag.is_null() { + let cancelled = unsafe { *cancel_flag } != 0; + if cancelled { + return to_cstring("cancelled"); + } + } + builder.add_file(&rel, data); + call_progress(progress_cb, idx + 1, total, &rel); + } + + match builder.build_with_progress(&output_archive, |_, _, _| {}) { + Ok(_) => ptr::null_mut(), + Err(e) => to_cstring(&e.to_string()), + } + } +} diff --git a/libs/bsapacker/.gitattributes b/libs/bsapacker/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/bsapacker/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/bsapacker/.github/workflows/build.yml b/libs/bsapacker/.github/workflows/build.yml new file mode 100644 index 0000000..a1435cc --- /dev/null +++ b/libs/bsapacker/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build BSA Packer + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build BSA Packer + id: build-bsa-packer + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase diff --git a/libs/bsapacker/.gitignore b/libs/bsapacker/.gitignore new file mode 100644 index 0000000..d86263e --- /dev/null +++ b/libs/bsapacker/.gitignore @@ -0,0 +1,8 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build +.vs +CMakeSettings.json +.vscode diff --git a/libs/bsapacker/.pre-commit-config.yaml b/libs/bsapacker/.pre-commit-config.yaml new file mode 100644 index 0000000..7bd1380 --- /dev/null +++ b/libs/bsapacker/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + # - repo: https://github.com/pre-commit/mirrors-clang-format + # rev: v19.1.5 + # hooks: + # - id: clang-format + # 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/bsapacker/CMakeLists.txt b/libs/bsapacker/CMakeLists.txt new file mode 100644 index 0000000..48f6a1e --- /dev/null +++ b/libs/bsapacker/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +set(MO2_CMAKE_DEPRECATED_UIBASE_INCLUDE ON) + +project(bsa_packer) + +add_subdirectory(src) + +set(BUILD_TESTING ${BUILD_TESTING} CACHE BOOL "build tests for bsapacker plugin") +if(BUILD_TESTING) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/libs/bsapacker/CMakePresets.json b/libs/bsapacker/CMakePresets.json new file mode 100644 index 0000000..0c06d1a --- /dev/null +++ b/libs/bsapacker/CMakePresets.json @@ -0,0 +1,63 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "testing" + } + }, + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/bsapacker/LICENSE.md b/libs/bsapacker/LICENSE.md new file mode 100644 index 0000000..15a90fc --- /dev/null +++ b/libs/bsapacker/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 PurpleFez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/bsapacker/README.md b/libs/bsapacker/README.md new file mode 100644 index 0000000..8b5d490 --- /dev/null +++ b/libs/bsapacker/README.md @@ -0,0 +1,2 @@ +# bsapacker +During development, I used Qt 5.12.5 for building and running it in MO2. It also depends upon QtConcurrent, so if you're finding MO explodes make sure you add that to the dll folder. diff --git a/libs/bsapacker/src/ArchiveAutoService.cpp b/libs/bsapacker/src/ArchiveAutoService.cpp new file mode 100644 index 0000000..4c89c69 --- /dev/null +++ b/libs/bsapacker/src/ArchiveAutoService.cpp @@ -0,0 +1,102 @@ +#include +#include +#include +#include +#include +#include + +#include "NexusId.h" + +#ifdef __linux__ +#include +#endif + +namespace BsaPacker +{ + static const char* gameIdFromNexusId(const int nexusId) + { + switch (nexusId) { + case NexusId::Morrowind: return "morrowind"; + case NexusId::Oblivion: return "oblivion"; + case NexusId::Fallout3: return "fo3"; + case NexusId::NewVegas: return "fonv"; + case NexusId::Skyrim: + case NexusId::Enderal: return "skyrimle"; + case NexusId::SkyrimSE: + case NexusId::EnderalSE: return "skyrimse"; + case NexusId::Fallout4: return "fo4-fo76"; + case NexusId::Starfield: return "starfield-v3"; + default: return "fo4-fo76"; + } + } + + static int includeModeFromArchiveType(const bsa_archive_type_e type) + { + switch (type) { + case bsa_archive_type_e::baFO4: return 1; // non-dds + case bsa_archive_type_e::baFO4dds: return 2; // dds only + case bsa_archive_type_e::baSF: return 1; // non-dds + case bsa_archive_type_e::baSFdds: return 2; // dds only + default: return 0; // all files + } + } + + bool ArchiveAutoService::CreateBSA(libbsarch::bs_archive_auto* archive, + const QString& archiveName, + const bsa_archive_type_e type, + const QString& sourceDir, + const int nexusId) const + { + const QString hostArchiveName = MOBase::normalizePathForHost(archiveName); + const QString hostSourceDir = MOBase::normalizePathForHost(sourceDir); + const char* gameId = gameIdFromNexusId(nexusId); + const int includeMode = includeModeFromArchiveType(type); + + QProgressDialog savingDialog; + savingDialog.setWindowFlags(savingDialog.windowFlags() & ~Qt::WindowCloseButtonHint); + savingDialog.setWindowTitle(QObject::tr("Writing Archive")); + savingDialog.setCancelButton(0); + QLabel text; + text.setText(QObject::tr("Writing %1").arg(hostArchiveName)); + savingDialog.setLabel(&text); + savingDialog.setRange(0, 0); + savingDialog.show(); + auto future = QtConcurrent::run([=]() -> bool { +#ifdef __linux__ + char* err = bsa_ffi_pack_dir_filtered( + hostSourceDir.toUtf8().constData(), + hostArchiveName.toUtf8().constData(), + gameId, + includeMode, + nullptr, + nullptr); + if (err == nullptr) { + qDebug() << "packed archive via bsa_ffi for" << hostArchiveName; + return true; + } + + qWarning() << "bsa_ffi primary pack failed for" << hostArchiveName + << ":" << QString::fromUtf8(err) + << "- falling back to libbsarch"; + bsa_ffi_string_free(err); +#endif + try { + archive->save_to_disk(hostArchiveName.toStdString()); + } catch (std::exception&) { + return false; + } + return true; + }); + while (!future.isFinished()) + { + QCoreApplication::processEvents(); + } + savingDialog.hide(); + + if (future.result()) { + return true; + } + + return false; + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/ArchiveBuildDirector.cpp b/libs/bsapacker/src/ArchiveBuildDirector.cpp new file mode 100644 index 0000000..300d570 --- /dev/null +++ b/libs/bsapacker/src/ArchiveBuildDirector.cpp @@ -0,0 +1,28 @@ +#include + +#include + +namespace BsaPacker +{ + ArchiveBuildDirector::ArchiveBuildDirector( + const ISettingsService* settingsService, + IArchiveBuilder* archiveFileBuildService) + : m_ArchiveFileBuildService(archiveFileBuildService), + m_SettingsService(settingsService) + { + } + + void ArchiveBuildDirector::Construct() + { + QProgressDialog dialog("Adding to archive", + "Abort", + 0, + static_cast(this->m_ArchiveFileBuildService->getFileCount())); + QObject::connect(&dialog, &QProgressDialog::canceled, this->m_ArchiveFileBuildService, &IArchiveBuilder::cancel); + QObject::connect(this->m_ArchiveFileBuildService, SIGNAL(valueChanged(int)), &dialog, SLOT(setValue(int))); + dialog.show(); + this->m_ArchiveFileBuildService->setShareData(true); + this->m_ArchiveFileBuildService->setFiles(); + dialog.close(); + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/ArchiveBuilderFactory.cpp b/libs/bsapacker/src/ArchiveBuilderFactory.cpp new file mode 100644 index 0000000..fc72695 --- /dev/null +++ b/libs/bsapacker/src/ArchiveBuilderFactory.cpp @@ -0,0 +1,59 @@ +#include + +#include +#include +#include +#include +#include "NexusId.h" + +namespace BsaPacker +{ + ArchiveBuilderFactory::ArchiveBuilderFactory(const IArchiveBuilderHelper* archiveBuilderHelper) + : m_ArchiveBuilderHelper(archiveBuilderHelper) + { + } + + std::vector ArchiveBuilderFactory::GetArchiveTypes(const IModDto* modDto) const + { + switch (modDto->NexusId()) { + case NexusId::Morrowind: + return std::vector { baTES3 }; + case NexusId::Oblivion: + return std::vector { baTES4 }; + case NexusId::Fallout3: + case NexusId::NewVegas: + case NexusId::Skyrim: + case NexusId::Enderal: + return std::vector { baFO3 }; + case NexusId::SkyrimSE: + case NexusId::EnderalSE: + return std::vector { baSSE }; + case NexusId::Fallout4: + return std::vector { baFO4, baFO4dds }; + case NexusId::Starfield: + return std::vector { baSF, baSFdds }; + default: + return std::vector { baNone }; + } + } + + std::unique_ptr ArchiveBuilderFactory::Create(const bsa_archive_type_e archiveType, const IModDto* modDto) const + { + switch (archiveType) { + case baTES3: + case baTES4: + case baFO3: + case baSSE: + return std::make_unique(this->m_ArchiveBuilderHelper, modDto->Directory(), archiveType); + case baFO4: + case baSF: + return std::make_unique(this->m_ArchiveBuilderHelper, modDto->Directory(), archiveType); + case baFO4dds: + case baSFdds: + return std::make_unique(this->m_ArchiveBuilderHelper, modDto->Directory(), archiveType); + case baNone: + default: + return std::make_unique(); + } + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/ArchiveBuilderHelper.cpp b/libs/bsapacker/src/ArchiveBuilderHelper.cpp new file mode 100644 index 0000000..c5c956b --- /dev/null +++ b/libs/bsapacker/src/ArchiveBuilderHelper.cpp @@ -0,0 +1,77 @@ +#include + +#include +#include + +#include "SettingsService.h" + +#include + +using std::filesystem::path; +using std::filesystem::directory_entry; +using std::filesystem::directory_iterator; +using std::filesystem::recursive_directory_iterator; + +namespace BsaPacker +{ + const std::set ArchiveBuilderHelper::INCOMPRESSIBLE_TYPES = { ".wav", ".ogg", ".mp3" }; + + ArchiveBuilderHelper::ArchiveBuilderHelper(const ISettingsService* settingsService) + : m_SettingsService(settingsService) + { + } + uint32_t ArchiveBuilderHelper::getFileCount(const path& rootDirectory) const + { + uint32_t count = 0; + for(auto& p : recursive_directory_iterator(rootDirectory)) { + if (p.is_regular_file()) { + count++; + } + } + return count; + } + + std::vector ArchiveBuilderHelper::getRootDirectoryFilenames(const path& rootDirectory) const + { + std::vector filenames; + for (const auto& entry : directory_iterator(rootDirectory)) { + filenames.push_back(entry.path().filename().native()); + } + return filenames; + } + + bool ArchiveBuilderHelper::isFileIgnorable(const path& filepath, const std::vector& rootDirFilenames) const + { + return this->doesPathContainFiles(filepath, rootDirFilenames) || // ignore files within mod directory + this->isExtensionBlacklisted(filepath); // ignore user blacklisted file types + } + + bool ArchiveBuilderHelper::isIncompressible(const path& filename) const + { + if (!this->m_SettingsService->GetPluginSetting(SettingsService::SETTING_COMPRESS_ARCHIVES).toBool()) { + return true; + } + + const auto& extension = filename.extension().string(); + const auto& count = ArchiveBuilderHelper::INCOMPRESSIBLE_TYPES.count(extension); + const auto& result = count > 0; + return result; + } + + bool ArchiveBuilderHelper::isExtensionBlacklisted(const path& filepath) const + { + const auto& setting = this->m_SettingsService->GetPluginSetting(SettingsService::SETTING_BLACKLISTED_FILES).toString().toStdString(); + const auto &extension = filepath.extension().string(); + const auto count = std::ranges::count( + setting | std::views::split(' '), + extension, + [](auto r) + { return std::string_view(r.data(), r.size()); }); + return count > 0; + } + + bool ArchiveBuilderHelper::doesPathContainFiles(const path& filepath, const std::vector& files) const + { + return std::find(files.begin(), files.end(), filepath.filename()) != files.end(); + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/ArchiveNameService.cpp b/libs/bsapacker/src/ArchiveNameService.cpp new file mode 100644 index 0000000..9cea91b --- /dev/null +++ b/libs/bsapacker/src/ArchiveNameService.cpp @@ -0,0 +1,75 @@ +#include "ArchiveNameService.h" + +#include "NexusId.h" + +#include + +namespace BsaPacker +{ + ArchiveNameService::ArchiveNameService(const IModContext* modContext) + : m_ModContext(modContext) + { + } + + QString ArchiveNameService::GetFileExtension() const + { + switch (this->m_ModContext->GetNexusId()) { + case NexusId::Morrowind: + case NexusId::Oblivion: + case NexusId::Fallout3: + case NexusId::NewVegas: + case NexusId::Skyrim: + case NexusId::SkyrimSE: + case NexusId::Enderal: + case NexusId::EnderalSE: + return QStringLiteral(".bsa"); + case NexusId::Fallout4: + case NexusId::Starfield: + return QStringLiteral(".ba2"); + default: + return QString(); + } + } + + QString ArchiveNameService::GetArchiveFullPath(const bsa_archive_type_e type, const IModDto* modDto) const + { + const QString& pathNoExt(QDir::toNativeSeparators(modDto->Directory() + '/' + modDto->ArchiveName() + this->Infix(type))); + const QString& suffix = this->Suffix(pathNoExt); + return QDir::toNativeSeparators(pathNoExt + suffix + this->GetFileExtension()); + } + + QString ArchiveNameService::Infix(const bsa_archive_type_e type) const + { + switch (type) { + case baFO4: + case baSF: + return QStringLiteral(" - Main"); + case baSFdds: + case baFO4dds: + return QStringLiteral(" - Textures"); + case baTES3: + case baTES4: + case baFO3: + case baSSE: + case baNone: + default: + return QString(); + }; + } + + // gets the number to append when there are multiple archives + // a way to avoid overwriting any existing files + QString ArchiveNameService::Suffix(const QString& pathNoExt) const { + int archiveIndex = 0; + const QString& fileExt = this->GetFileExtension(); + QFileInfo fileInfo(pathNoExt + fileExt); + while (fileInfo.exists()) { + ++archiveIndex; + fileInfo.setFile(pathNoExt + QString::number(archiveIndex) + fileExt); + } + if (archiveIndex != 0) { + return QString::number(archiveIndex); + } + return QString(); + } +} diff --git a/libs/bsapacker/src/ArchiveNameService.h b/libs/bsapacker/src/ArchiveNameService.h new file mode 100644 index 0000000..1ee8490 --- /dev/null +++ b/libs/bsapacker/src/ArchiveNameService.h @@ -0,0 +1,24 @@ +#ifndef ARCHIVENAMESERVICE_H +#define ARCHIVENAMESERVICE_H + +#include "bsapacker_global.h" +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT ArchiveNameService : public IArchiveNameService + { + public: + ArchiveNameService(const IModContext* modContext); + ~ArchiveNameService() override = default; + QString GetArchiveFullPath(bsa_archive_type_e type, const IModDto* modDto) const override; + QString GetFileExtension() const override; + QString Infix(bsa_archive_type_e type) const override; + QString Suffix(const QString& pathNoExt) const override; + private: + const IModContext* m_ModContext = nullptr; + }; +} + +#endif // ARCHIVENAMESERVICE_H diff --git a/libs/bsapacker/src/BSArchive.cpp b/libs/bsapacker/src/BSArchive.cpp new file mode 100644 index 0000000..6ce4278 --- /dev/null +++ b/libs/bsapacker/src/BSArchive.cpp @@ -0,0 +1,184 @@ +#include + +BSArchive::BSArchive() + : _archive(bsa_create()) +{ +} + +BSArchive::~BSArchive() +{ + if (_openedArchive) + close(); + free(); +} + +void BSArchive::free() +{ + LOG_LIBBSARCH << "Freeing archive: " << _archive; + + const auto &result = bsa_free(_archive); + _openedArchive = false; + + QLibBsarch::checkResult(result); +} + +void BSArchive::open(const QString &archivePath) +{ + LOG_LIBBSARCH << "Opening archive: " << archivePath; + + const auto &result = bsa_load_from_file(_archive, PREPARE_PATH_LIBBSARCH(archivePath)); + _openedArchive = true; + + QLibBsarch::checkResult(result); +} + +void BSArchive::close() +{ + LOG_LIBBSARCH << "Closing archive: " << _archive; + + bsa_close(_archive); + _openedArchive = false; +} + +void BSArchive::create(const QString &archiveName, const bsa_archive_type_e& type, const BSArchiveEntries& entries) +{ + LOG_LIBBSARCH << "Creating archive. Archive name: " << archiveName; + LOG_LIBBSARCH << "type: " << type; + LOG_LIBBSARCH << "entries: " << entries.getEntries(); + + bsa_create_archive(_archive, PREPARE_PATH_LIBBSARCH(archiveName), type, entries.getEntries()); + + _openedArchive = true; +} + +void BSArchive::save() +{ + LOG_LIBBSARCH << "Saving archive: " << _archive; + + const auto &result = bsa_save(_archive); + + QLibBsarch::checkResult(result); +} + +void BSArchive::addFileFromDiskRoot(const QString &rootDir, const QString &filename) +{ + LOG_LIBBSARCH << "Adding file from disk root. Root directory: " << rootDir; + LOG_LIBBSARCH << "Filename: " << filename; + LOG_LIBBSARCH << "Archive: " << _archive; + + const auto &result = bsa_add_file_from_disk_root(_archive, + PREPARE_PATH_LIBBSARCH(rootDir), + PREPARE_PATH_LIBBSARCH(filename)); + + QLibBsarch::checkResult(result); +} + +void BSArchive::addFileFromDiskRoot(const QString &rootDir, const QStringList &files) +{ + for (const auto &file : files) + addFileFromDiskRoot(rootDir, file); +} + +void BSArchive::addFileFromDisk(const QString &pathInArchive, const QString &filePath) +{ + LOG_LIBBSARCH << "Adding file from disk root. Path in archive: " << pathInArchive; + LOG_LIBBSARCH << "Filepath: " << filePath; + LOG_LIBBSARCH << "Archive: " << _archive; + + const auto &result = bsa_add_file_from_disk_root(_archive, + PREPARE_PATH_LIBBSARCH(pathInArchive), + PREPARE_PATH_LIBBSARCH(filePath)); + + QLibBsarch::checkResult(result); +} + +void BSArchive::addFileFromMemory(const QString &filename, const QByteArray &data) //NOTE UNTESTED +{ + LOG_LIBBSARCH << "Adding file from memory. Filename: " << filename; + LOG_LIBBSARCH << "Data size: " << data.size(); + + uint32_t size = static_cast(data.size()); + bsa_buffer_t buffer = const_cast(data.data()); + const auto &result = bsa_add_file_from_memory(_archive, PREPARE_PATH_LIBBSARCH(filename), size, buffer); + + QLibBsarch::checkResult(result); +} +void BSArchive::setCompressed(bool value) +{ + LOG_LIBBSARCH << "Setting compressed flag to " << value; + bsa_compress_set(_archive, value); +} + +void BSArchive::setShareData(bool value) +{ + LOG_LIBBSARCH << "Setting share data flag to " << value; + bsa_share_data_set(_archive, value); +} + +bsa_file_record_t BSArchive::findFileRecord(const QString &filename) +{ + LOG_LIBBSARCH << "Finding file record of: " << filename; + const auto &result = bsa_find_file_record(_archive, PREPARE_PATH_LIBBSARCH(filename)); + return result; +} + +QByteArray BSArchive::extractFileDataByRecord(bsa_file_record_t record) +{ + const auto &result = bsa_extract_file_data_by_record(_archive, record); + + QLibBsarch::checkResult(result); + + char *buffer = static_cast(result.buffer.data); + int size = static_cast(result.buffer.size); + + QByteArray byte_array(buffer, size); + return byte_array; +} + +QByteArray BSArchive::extractFileDataByFilename(const QString &filename) +{ + const auto &result = bsa_extract_file_data_by_filename(_archive, PREPARE_PATH_LIBBSARCH(filename)); + + QLibBsarch::checkResult(result); + + char *buffer = static_cast(result.buffer.data); + int size = static_cast(result.buffer.size); + + QByteArray byte_array(buffer, size); + return byte_array; +} + +void BSArchive::extract(const QString &filename, const QString &saveAs) +{ + qDebug() << "Extracting: " << filename << " saved as " << saveAs; + const auto &result = bsa_extract_file(_archive, PREPARE_PATH_LIBBSARCH(filename), PREPARE_PATH_LIBBSARCH(saveAs)); + + QLibBsarch::checkResult(result); +} + +QStringList BSArchive::listFiles() +{ + bsa_entry_list_t list = bsa_entry_list_create(); + const auto &result = bsa_get_resource_list(_archive, list, L""); + + QLibBsarch::checkResult(result); + + return BSArchiveEntries(list).list(); +} + +bsa_archive_t BSArchive::getArchive() const +{ + return _archive; +} + +void BSArchive::reset() +{ + free(); + _archive = bsa_create(); +} + +void BSArchive::setDDSCallback(bsa_file_dds_info_proc_t file_dds_info_proc, void *context) +{ + LOG_LIBBSARCH << "Setting DDS callback for archive: " << _archive << "\nCallback adress: " << &file_dds_info_proc; + bsa_file_dds_info_callback_set(_archive, file_dds_info_proc, context); +} diff --git a/libs/bsapacker/src/BSArchiveAuto.cpp b/libs/bsapacker/src/BSArchiveAuto.cpp new file mode 100644 index 0000000..f2ef7f8 --- /dev/null +++ b/libs/bsapacker/src/BSArchiveAuto.cpp @@ -0,0 +1,101 @@ +#include +#include + +BSArchiveAuto::BSArchiveAuto(const QString &rootDirectory) + : _rootDirectory(QDir::toNativeSeparators(QDir::cleanPath(rootDirectory))) +{ +} + +void BSArchiveAuto::open(const QString &archivePath) +{ + _archive.open(archivePath); + qDebug() << "Opening archive: " << archivePath; +} + +void BSArchiveAuto::create(const QString &archiveName, const bsa_archive_type_e &type) +{ + _archive.create(archiveName, type, _entries); + + for (auto it = _filesfromMemory.constBegin(); it != _filesfromMemory.constEnd(); ++it) + _archive.addFileFromMemory(it.key(), it.value()); + + for (const auto &file : _filesFromDiskRoot) + _archive.addFileFromDiskRoot(_rootDirectory.path(), file); + + for (auto it = _filesFromDisk.constBegin(); it != _filesFromDisk.constEnd(); ++it) + _archive.addFileFromDisk(it.key(), it.value()); +} + + +void BSArchiveAuto::addFileFromDiskRoot(const QString &filename) +{ + _entries.add(_rootDirectory.relativeFilePath(filename)); + _filesFromDiskRoot << filename; +} + + +void BSArchiveAuto::addFileFromDiskRoot(const QStringList& files) +{ + for (const auto &file : files) + addFileFromDiskRoot(file); +} + +void BSArchiveAuto::addFileFromDisk(const QString &saveAs, const QString &diskPath) +{ + _entries.add(saveAs); + _filesFromDisk.insert(saveAs, diskPath); +} + +void BSArchiveAuto::addFileFromDisk(const QMap &map) +{ + for (auto it = map.cbegin(); it != map.cend(); ++it) + _filesFromDisk.insert(it.key(), it.value()); +} + +void BSArchiveAuto::addFileFromMemory(const QString &filename, const QByteArray &data) +{ + _entries.add(filename); + _filesfromMemory.insert(filename, data); +} + +void BSArchiveAuto::extractAll(const QString& destinationDirectory, const bool &overwriteExistingFiles) +{ + for (const auto &file : _archive.listFiles()) + { + QFile currentFile(destinationDirectory + "/" + file); + _rootDirectory.mkpath(destinationDirectory + "/" + QFileInfo(file).path()); + if(currentFile.exists() && overwriteExistingFiles) + { + currentFile.remove(); + _archive.extract(file, currentFile.fileName()); + } + else if (!currentFile.exists()) + _archive.extract(file, currentFile.fileName()); + } +} + +void BSArchiveAuto::save() +{ + _archive.save(); +} + +void BSArchiveAuto::reset() +{ + _archive.reset(); + _entries.reset(); +} + +void BSArchiveAuto::setShareData(const bool state) +{ + _archive.setShareData(state); +} + +void BSArchiveAuto::setCompressed(const bool state) +{ + _archive.setCompressed(state); +} + +void BSArchiveAuto::setDDSCallback(bsa_file_dds_info_proc_t fileDDSInfoProc, void *context) +{ + _archive.setDDSCallback(fileDDSInfoProc, context); +} diff --git a/libs/bsapacker/src/BSArchiveEntries.cpp b/libs/bsapacker/src/BSArchiveEntries.cpp new file mode 100644 index 0000000..64ef838 --- /dev/null +++ b/libs/bsapacker/src/BSArchiveEntries.cpp @@ -0,0 +1,65 @@ +#include + +BSArchiveEntries::BSArchiveEntries() + : _entries(bsa_entry_list_create()) +{ +} + +BSArchiveEntries::BSArchiveEntries(const QStringList &QSLEntries) + : _entries(bsa_entry_list_create()) +{ + for (const auto &entry : QSLEntries) + add (entry); +} + +BSArchiveEntries::BSArchiveEntries(const bsa_entry_list_t &entries) + : _entries(entries) +{ +} + +BSArchiveEntries::~BSArchiveEntries() +{ + free(); +} + +void BSArchiveEntries::free() +{ + bsa_entry_list_free(_entries); +} + +void BSArchiveEntries::reset() +{ + free(); + _entries = bsa_entry_list_create(); +} + +void BSArchiveEntries::add(const QString &filepath) +{ + LOG_LIBBSARCH << "Adding to entries: " << filepath; + + const auto &result = bsa_entry_list_add(_entries, PREPARE_PATH_LIBBSARCH(filepath)); + + QLibBsarch::checkResult(result); +} + +uint32_t BSArchiveEntries::count() +{ + return bsa_entry_list_count(_entries); +} + +QStringList BSArchiveEntries::list() +{ + QStringList list; + for(uint32_t i = 0 ; i < count() ; ++i) + { + wchar_t buffer[1024]; + bsa_entry_list_get(_entries, i, 1024, buffer); + list << QString::fromWCharArray(buffer); + } + return list; +} + +bsa_entry_list_t BSArchiveEntries::getEntries() const +{ + return _entries; +} diff --git a/libs/bsapacker/src/BsaPacker.cpp b/libs/bsapacker/src/BsaPacker.cpp new file mode 100644 index 0000000..f356b78 --- /dev/null +++ b/libs/bsapacker/src/BsaPacker.cpp @@ -0,0 +1,99 @@ +#include + +#include +#include +#include +#include +#include +#include "BsaPackerWorker.h" +#include "DummyPluginLogic.h" +#include "DummyPluginServiceFactory.h" +#include "FileWriterService.h" +#include "HideLooseAssetService.h" +#include "ModContext.h" +#include "ModDto.h" +#include "OverrideFileService.h" +#include "PackerDialog.h" +#include "SettingsService.h" +#include +#include +#include + +namespace BsaPacker +{ + bool Bsa_Packer::init(MOBase::IOrganizer* moInfo) + { + this->m_Organizer = moInfo; + this->m_ModContext = std::make_unique(this->m_Organizer); + this->m_SettingsService = std::make_unique(this->m_Organizer); + return true; + } + + QString Bsa_Packer::name() const + { + return QStringLiteral("BSA Packer"); + } + + QString Bsa_Packer::author() const + { + return QStringLiteral("MattyFez & MO2 Team"); + } + + QString Bsa_Packer::description() const + { + return tr("Transform loose files into a Bethesda Softworks Archive file (.bsa/.ba2)."); + } + + MOBase::VersionInfo Bsa_Packer::version() const + { + return MOBase::VersionInfo(1, 1, 0, MOBase::VersionInfo::RELEASE_FINAL); + } + + QList Bsa_Packer::settings() const + { + return SettingsService::PluginSettings; + } + + QString Bsa_Packer::tooltip() const + { + return tr("Transform loose files into a Bethesda Softworks Archive file (.bsa/.ba2)."); + } + + QIcon Bsa_Packer::icon() const + { + return QIcon(); + } + + QString Bsa_Packer::displayName() const + { + return tr("BSA Packer"); + } + + void Bsa_Packer::display() const + { + ArchiveBuilderHelper archiveBuilderHelper(this->m_SettingsService.get()); + ArchiveBuilderFactory archiveBuilderFactory(&archiveBuilderHelper); + ArchiveAutoService archiveAutoService; + FileWriterService fileWriterService; + ArchiveNameService archiveNameService(this->m_ModContext.get()); + DummyPluginLogic dummyPluginLogic(this->m_SettingsService.get(), &archiveNameService); + DummyPluginServiceFactory dummyPluginServiceFactory( + this->m_ModContext.get(), &fileWriterService, &dummyPluginLogic); + HideLooseAssetService hideLooseAssetService(this->m_SettingsService.get()); + PackerDialog packerDialog(this->m_ModContext.get()); + ModDtoFactory modDtoFactory(this->m_ModContext.get(), &packerDialog); + OverrideFileService overrideFileService(&fileWriterService); + + BsaPackerWorker worker( + this->m_SettingsService.get(), + &modDtoFactory, + &archiveBuilderFactory, + &archiveAutoService, + &dummyPluginServiceFactory, + &hideLooseAssetService, + &archiveNameService, + &overrideFileService); + worker.DoWork(); + } + +} // namespace BsaPacker diff --git a/libs/bsapacker/src/BsaPacker.h b/libs/bsapacker/src/BsaPacker.h new file mode 100644 index 0000000..a624586 --- /dev/null +++ b/libs/bsapacker/src/BsaPacker.h @@ -0,0 +1,42 @@ +#ifndef BSA_PACKER_H +#define BSA_PACKER_H + +#include "bsapacker_global.h" +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT Bsa_Packer : public MOBase::IPluginTool + { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.MattyFez.BSA_Packer" FILE "bsa_packer.json") +#endif + + public: + // IPlugin interface + bool init(MOBase::IOrganizer* moInfo) override; + [[nodiscard]] QString name() const override; + [[nodiscard]] QString author() const override; + [[nodiscard]] QString description() const override; + [[nodiscard]] MOBase::VersionInfo version() const override; + [[nodiscard]] QList settings() const override; + + // IPluginTool interface + [[nodiscard]] QString displayName() const override; + [[nodiscard]] QString tooltip() const override; + [[nodiscard]] QIcon icon() const override; + + public Q_SLOTS: + void display() const override; + + private: + MOBase::IOrganizer* m_Organizer = nullptr; + std::unique_ptr m_ModContext = nullptr; + std::unique_ptr m_SettingsService = nullptr; + }; +} // namespace BsaPacker +#endif //BSA_PACKER_H diff --git a/libs/bsapacker/src/BsaPackerWorker.cpp b/libs/bsapacker/src/BsaPackerWorker.cpp new file mode 100644 index 0000000..3b9e558 --- /dev/null +++ b/libs/bsapacker/src/BsaPackerWorker.cpp @@ -0,0 +1,66 @@ +#include "BsaPackerWorker.h" + +#include + +#include +#include + +namespace BsaPacker +{ + BsaPackerWorker::BsaPackerWorker( + const ISettingsService* settingsService, + const IModDtoFactory* modDtoFactory, + const IArchiveBuilderFactory* archiveBuilderFactory, + const IArchiveAutoService* archiveAutoService, + const IDummyPluginServiceFactory* dummyPluginServiceFactory, + const IHideLooseAssetService* hideLooseAssetService, + const IArchiveNameService* archiveNameService, + const IOverrideFileService* overrideFileService) : + m_SettingsService(settingsService), + m_ModDtoFactory(modDtoFactory), + m_ArchiveBuilderFactory(archiveBuilderFactory), + m_ArchiveAutoService(archiveAutoService), + m_DummyPluginServiceFactory(dummyPluginServiceFactory), + m_HideLooseAssetService(hideLooseAssetService), + m_ArchiveNameService(archiveNameService), + m_OverrideFileService(overrideFileService) + { + } + + void BsaPackerWorker::DoWork() const + { + QStringList createdArchives; + const std::unique_ptr modDto = this->m_ModDtoFactory->Create(); // handles PackerDialog and validation, implements Null Object pattern + const std::vector types = this->m_ArchiveBuilderFactory->GetArchiveTypes(modDto.get()); + for (auto&& type : types) { + const std::unique_ptr builder = this->m_ArchiveBuilderFactory->Create(type, modDto.get()); + ArchiveBuildDirector director(this->m_SettingsService, builder.get()); + director.Construct(); // must check if cancelled + const std::vector> archives = builder->getArchives(); + for (const auto& archive : archives) { + if (archive) { + const QFileInfo fileInfo(this->m_ArchiveNameService->GetArchiveFullPath(type, modDto.get())); + bool res = this->m_ArchiveAutoService->CreateBSA( + archive.get(), fileInfo.absoluteFilePath(), type, + modDto->Directory(), modDto->NexusId()); + if (res) { + createdArchives.append(fileInfo.completeBaseName()); + } + } + } + } + + if (!createdArchives.isEmpty()) { + QMessageBox::information(nullptr, "", + QObject::tr("Created archive(s):") + "\n" + createdArchives.join(modDto->ArchiveExtension() +",\n") + modDto->ArchiveExtension()); + this->m_OverrideFileService->CreateOverrideFile(modDto->NexusId(), modDto->Directory(), createdArchives); + } + + const std::unique_ptr pluginService = this->m_DummyPluginServiceFactory->Create(); + pluginService->CreatePlugin(modDto->Directory(), modDto->ArchiveName()); + + if (!modDto->Directory().isEmpty()) { + this->m_HideLooseAssetService->HideLooseAssets(modDto->Directory()); + } + } +} diff --git a/libs/bsapacker/src/BsaPackerWorker.h b/libs/bsapacker/src/BsaPackerWorker.h new file mode 100644 index 0000000..1610e8b --- /dev/null +++ b/libs/bsapacker/src/BsaPackerWorker.h @@ -0,0 +1,42 @@ +#ifndef BSAPACKERWORKER_H +#define BSAPACKERWORKER_H + +#include "bsapacker_global.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT BsaPackerWorker + { + public: + BsaPackerWorker( + const ISettingsService* settingsService, + const IModDtoFactory* modDtoFactory, + const IArchiveBuilderFactory* archiveBuilderFactory, + const IArchiveAutoService* archiveAutoService, + const IDummyPluginServiceFactory* dummyPluginService, + const IHideLooseAssetService* hideLooseAssetService, + const IArchiveNameService* archiveNameService, + const IOverrideFileService* overrideFileService); + void DoWork() const; + + private: + const ISettingsService* m_SettingsService = nullptr; + const IModDtoFactory* m_ModDtoFactory = nullptr; + const IArchiveBuilderFactory* m_ArchiveBuilderFactory = nullptr; + const IArchiveAutoService* m_ArchiveAutoService = nullptr; + const IDummyPluginServiceFactory* m_DummyPluginServiceFactory = nullptr; + const IHideLooseAssetService* m_HideLooseAssetService = nullptr; + const IArchiveNameService* m_ArchiveNameService = nullptr; + const IOverrideFileService* m_OverrideFileService = nullptr; + }; +} + +#endif // BSAPACKERWORKER_H diff --git a/libs/bsapacker/src/CMakeLists.txt b/libs/bsapacker/src/CMakeLists.txt new file mode 100644 index 0000000..f002b13 --- /dev/null +++ b/libs/bsapacker/src/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Qt6 REQUIRED COMPONENTS Concurrent) +find_path(BEXT_DI_INCLUDE_DIRS "boost/di.hpp") + +file(GLOB bsapacker_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc + ${CMAKE_CURRENT_SOURCE_DIR}/bsapacker/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/qlibbsarch/*.h +) + +add_library(bsa_packer SHARED ${bsapacker_SOURCES}) +mo2_configure_plugin(bsa_packer NO_SOURCES WARNINGS OFF) + +target_link_libraries(bsa_packer PUBLIC mo2::uibase Qt6::Concurrent libbsarch_OOP) +target_include_directories(bsa_packer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +if(BEXT_DI_INCLUDE_DIRS) + target_include_directories(bsa_packer PUBLIC ${BEXT_DI_INCLUDE_DIRS}) +endif() +target_compile_definitions(bsa_packer PRIVATE BSAPACKER_LIBRARY) + +if(TARGET mo2::bsa_ffi) + target_link_libraries(bsa_packer PUBLIC mo2::bsa_ffi) +endif() + +# need to deploy this here as MO2 does not depends on Qt6::Concurrent +install(FILES $ DESTINATION bin/dlls) + +# this is done by modorganizer itself now +# +# install(FILES $ DESTINATION bin/dlls) + +mo2_install_plugin(bsa_packer) diff --git a/libs/bsapacker/src/DummyPluginLogic.cpp b/libs/bsapacker/src/DummyPluginLogic.cpp new file mode 100644 index 0000000..a8e73ff --- /dev/null +++ b/libs/bsapacker/src/DummyPluginLogic.cpp @@ -0,0 +1,58 @@ +#include "DummyPluginLogic.h" + +#include +#include +#include "SettingsService.h" + +namespace BsaPacker +{ + DummyPluginLogic::DummyPluginLogic( + const ISettingsService* settingsService, + const IArchiveNameService* archiveNameService) + : m_SettingsService(settingsService), + m_ArchiveNameService(archiveNameService) + { + } + + bool DummyPluginLogic::canCreateDummyESP(const QString& fileNameNoExtension, const bsa_archive_type_e type) const + { + const std::array& fileList = { + fileNameNoExtension + ".esm", + fileNameNoExtension + ".esp" + }; + return this->canCreateDummy(fileList, fileNameNoExtension, type); + } + + bool DummyPluginLogic::canCreateDummyESL(const QString& fileNameNoExtension, const bsa_archive_type_e type) const + { + const std::array& fileList = { + fileNameNoExtension + ".esm", + fileNameNoExtension + ".esp", + fileNameNoExtension + ".esl" + }; + return this->canCreateDummy(fileList, fileNameNoExtension, type); + } + + template + bool DummyPluginLogic::canCreateDummy(const std::array& fileList, + const QString& fileNameNoExtension, + const bsa_archive_type_e type) const + { + const QFileInfo archive(fileNameNoExtension + this->m_ArchiveNameService->Infix(type) + this->m_ArchiveNameService->GetFileExtension()); + if (!(archive.exists() && archive.isFile())) { + return false; + } + + if (!this->m_SettingsService->GetPluginSetting(SettingsService::SETTING_CREATE_PLUGINS).toBool()) { + return false; + } + + for (const QString& info : fileList) { + const QFileInfo fileInfo(info); + if (fileInfo.exists() && fileInfo.isFile()) { + return false; + } + } + return true; + } +} diff --git a/libs/bsapacker/src/DummyPluginLogic.h b/libs/bsapacker/src/DummyPluginLogic.h new file mode 100644 index 0000000..ee9e893 --- /dev/null +++ b/libs/bsapacker/src/DummyPluginLogic.h @@ -0,0 +1,30 @@ +#ifndef DUMMYPLUGINLOGIC_H +#define DUMMYPLUGINLOGIC_H + +#include +#include +#include + +namespace BsaPacker +{ + class DummyPluginLogic : public IDummyPluginLogic + { + public: + DummyPluginLogic(const ISettingsService* settingsService, + const IArchiveNameService* archiveNameService); + ~DummyPluginLogic() override = default; + [[nodiscard]] bool canCreateDummyESP(const QString& fileNameNoExtension, const bsa_archive_type_e type) const override; + [[nodiscard]] bool canCreateDummyESL(const QString& fileNameNoExtension, const bsa_archive_type_e type) const override; + + private: + template + bool canCreateDummy(const std::array& fileList, + const QString& fileNameNoExtension, + const bsa_archive_type_e type) const; + + const ISettingsService* m_SettingsService = nullptr; + const IArchiveNameService* m_ArchiveNameService = nullptr; + }; +} + +#endif // DUMMYPLUGINLOGIC_H diff --git a/libs/bsapacker/src/DummyPluginServiceFactory.cpp b/libs/bsapacker/src/DummyPluginServiceFactory.cpp new file mode 100644 index 0000000..fcd0705 --- /dev/null +++ b/libs/bsapacker/src/DummyPluginServiceFactory.cpp @@ -0,0 +1,54 @@ +#include "DummyPluginServiceFactory.h" + +#include "NexusId.h" + +#include "Fallout3DummyPluginService.h" +#include "Fallout4DummyPluginService.h" +#include "NewVegasDummyPluginService.h" +#include "NullDummyPluginService.h" +#include "OblivionDummyPluginService.h" +#include "SkyrimDummyPluginService.h" +#include "SkyrimSEDummyPluginService.h" +#include "StarfieldDummyPluginService.h" + +#include + +namespace BsaPacker +{ + DummyPluginServiceFactory::DummyPluginServiceFactory( + const IModContext* modContext, + const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic) + : m_ModContext(modContext), + m_FileWriterService(fileWriterService), + m_DummyPluginLogic(dummyPluginLogic) + { + } + + std::unique_ptr DummyPluginServiceFactory::Create() const + { + // violates SRP! Must seperate choosing logic from construction. This class is not a factory, but a strategy! + switch (this->m_ModContext->GetNexusId()) { + case NexusId::Oblivion: + return std::make_unique(m_FileWriterService, m_DummyPluginLogic); + case NexusId::Fallout3: + return std::make_unique(m_FileWriterService, m_DummyPluginLogic); + case NexusId::NewVegas: + return std::make_unique(m_FileWriterService, m_DummyPluginLogic); + case NexusId::Skyrim: + case NexusId::Enderal: + return std::make_unique(m_FileWriterService, m_DummyPluginLogic); + case NexusId::SkyrimSE: + case NexusId::EnderalSE: + return std::make_unique(m_FileWriterService, m_DummyPluginLogic); + case NexusId::Fallout4: + return std::make_unique(m_FileWriterService, m_DummyPluginLogic); + case NexusId::Starfield: + return std::make_unique(m_FileWriterService, m_DummyPluginLogic); + case NexusId::Morrowind: + default: + break; + } + return std::make_unique(); + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/DummyPluginServiceFactory.h b/libs/bsapacker/src/DummyPluginServiceFactory.h new file mode 100644 index 0000000..219c023 --- /dev/null +++ b/libs/bsapacker/src/DummyPluginServiceFactory.h @@ -0,0 +1,29 @@ +#ifndef DUMMYPLUGINSERVICEFACTORY_H +#define DUMMYPLUGINSERVICEFACTORY_H + +#include "bsapacker_global.h" +#include +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT DummyPluginServiceFactory : public IDummyPluginServiceFactory + { + public: + explicit DummyPluginServiceFactory( + const IModContext* modContext, + const IFileWriterService* fileWriterService, // bad! only passing this to another constructor! + const IDummyPluginLogic* dummyPluginLogic); // TODO: perhaps remedy with strategy pattern? + ~DummyPluginServiceFactory() override = default; + [[nodiscard]] std::unique_ptr Create() const override; + + private: + const IModContext* m_ModContext = nullptr; + const IFileWriterService* m_FileWriterService = nullptr; + const IDummyPluginLogic* m_DummyPluginLogic = nullptr; + }; +} // namespace BsaPacker + +#endif // DUMMYPLUGINSERVICEFACTORY_H diff --git a/libs/bsapacker/src/Fallout3DummyPluginService.cpp b/libs/bsapacker/src/Fallout3DummyPluginService.cpp new file mode 100644 index 0000000..db90eeb --- /dev/null +++ b/libs/bsapacker/src/Fallout3DummyPluginService.cpp @@ -0,0 +1,26 @@ +#include "Fallout3DummyPluginService.h" + +namespace BsaPacker +{ + Fallout3DummyPluginService::Fallout3DummyPluginService( + const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic) + : m_FileWriterService(fileWriterService), + m_DummyPluginLogic(dummyPluginLogic) + { + } + + bool Fallout3DummyPluginService::CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const + { + const QString& fileNameNoExtension = modPath + '/' + archiveNameBase; + if (!this->m_DummyPluginLogic->canCreateDummyESP(fileNameNoExtension, bsa_archive_type_e::baFO3)) + { + return false; + } + const std::string& absoluteFileName = fileNameNoExtension.toStdString() + ".esp"; + return this->m_FileWriterService->Write(absoluteFileName, + reinterpret_cast(Fallout3DummyPluginService::RAW_FALLOUT3), + sizeof(Fallout3DummyPluginService::RAW_FALLOUT3)); + } +} diff --git a/libs/bsapacker/src/Fallout3DummyPluginService.h b/libs/bsapacker/src/Fallout3DummyPluginService.h new file mode 100644 index 0000000..83103ad --- /dev/null +++ b/libs/bsapacker/src/Fallout3DummyPluginService.h @@ -0,0 +1,41 @@ +#ifndef FALLOUT3DUMMYPLUGINSERVICE_H +#define FALLOUT3DUMMYPLUGINSERVICE_H + +#include "bsapacker_global.h" +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT Fallout3DummyPluginService : public IDummyPluginService + { + public: + Fallout3DummyPluginService(const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic); + ~Fallout3DummyPluginService() override = default; + bool CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const override; + + static constexpr unsigned char RAW_FALLOUT3[] = { + 0x54, 0x45, 0x53, 0x34, 0x41, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0x48, 0x45, 0x44, 0x52, 0x0C, 0x00, 0xD7, 0xA3, + 0x70, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xA6, 0x0E, + 0x00, 0x00, 0x43, 0x4E, 0x41, 0x4D, 0x08, 0x00, + 0x44, 0x45, 0x46, 0x41, 0x55, 0x4C, 0x54, 0x00, + 0x4D, 0x41, 0x53, 0x54, 0x0D, 0x00, 0x46, 0x61, + 0x6C, 0x6C, 0x6F, 0x75, 0x74, 0x33, 0x2E, 0x65, + 0x73, 0x6D, 0x00, 0x44, 0x41, 0x54, 0x41, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, + }; + + private: + const IFileWriterService* m_FileWriterService = nullptr; + const IDummyPluginLogic* m_DummyPluginLogic = nullptr; + }; +} + +#endif // FALLOUT3DUMMYPLUGINSERVICE_H diff --git a/libs/bsapacker/src/Fallout4DummyPluginService.cpp b/libs/bsapacker/src/Fallout4DummyPluginService.cpp new file mode 100644 index 0000000..844ab21 --- /dev/null +++ b/libs/bsapacker/src/Fallout4DummyPluginService.cpp @@ -0,0 +1,26 @@ +#include "Fallout4DummyPluginService.h" + +namespace BsaPacker +{ + Fallout4DummyPluginService::Fallout4DummyPluginService( + const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic) + : m_FileWriterService(fileWriterService), + m_DummyPluginLogic(dummyPluginLogic) + { + } + + bool Fallout4DummyPluginService::CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const + { + const QString& fileNameNoExtension = modPath + '/' + archiveNameBase; + if (!this->m_DummyPluginLogic->canCreateDummyESL(fileNameNoExtension, bsa_archive_type_e::baFO4) && !this->m_DummyPluginLogic->canCreateDummyESL(fileNameNoExtension, bsa_archive_type_e::baFO4dds)) + { + return false; + } + const std::string& absoluteFileName = fileNameNoExtension.toStdString() + ".esp"; + return this->m_FileWriterService->Write(absoluteFileName, + reinterpret_cast(Fallout4DummyPluginService::RAW_FALLOUT4), + sizeof(Fallout4DummyPluginService::RAW_FALLOUT4)); + } +} diff --git a/libs/bsapacker/src/Fallout4DummyPluginService.h b/libs/bsapacker/src/Fallout4DummyPluginService.h new file mode 100644 index 0000000..534dd5c --- /dev/null +++ b/libs/bsapacker/src/Fallout4DummyPluginService.h @@ -0,0 +1,34 @@ +#ifndef FALLOUT4DUMMYPLUGINSERVICE_H +#define FALLOUT4DUMMYPLUGINSERVICE_H + +#include "bsapacker_global.h" +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT Fallout4DummyPluginService : public IDummyPluginService + { + public: + Fallout4DummyPluginService(const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic); + ~Fallout4DummyPluginService() override = default; + bool CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const override; + + static constexpr unsigned char RAW_FALLOUT4[] = { + 0x54, 0x45, 0x53, 0x34, 0x19, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, + 0x48, 0x45, 0x44, 0x52, 0x0C, 0x00, 0x33, 0x33, 0x73, 0x3F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x43, 0x4E, 0x41, 0x4D, 0x01, 0x00, + 0x00 + }; + + private: + const IFileWriterService* m_FileWriterService = nullptr; + const IDummyPluginLogic* m_DummyPluginLogic = nullptr; + }; +} + +#endif // FALLOUT4DUMMYPLUGINSERVICE_H diff --git a/libs/bsapacker/src/FileWriterService.cpp b/libs/bsapacker/src/FileWriterService.cpp new file mode 100644 index 0000000..0863ccd --- /dev/null +++ b/libs/bsapacker/src/FileWriterService.cpp @@ -0,0 +1,18 @@ +#include "FileWriterService.h" + +#include + +namespace BsaPacker +{ + bool FileWriterService::Write(const std::string& path, const char* payload, const uint32_t size) const + { + std::ofstream file; + file.open(path, std::ios::out | std::ios::binary); + if (file.is_open()) { + file.write(reinterpret_cast(payload), size); + file.close(); + return true; + } + return false; + } +} diff --git a/libs/bsapacker/src/FileWriterService.h b/libs/bsapacker/src/FileWriterService.h new file mode 100644 index 0000000..b63b563 --- /dev/null +++ b/libs/bsapacker/src/FileWriterService.h @@ -0,0 +1,16 @@ +#ifndef FILEWRITERSERVICE_H +#define FILEWRITERSERVICE_H + +#include "bsapacker_global.h" +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT FileWriterService : public IFileWriterService + { + public: + FileWriterService() = default; + bool Write(const std::string& path, const char* payload, const uint32_t size) const override; + }; +} +#endif // FILEWRITERSERVICE_H diff --git a/libs/bsapacker/src/GeneralArchiveBuilder.cpp b/libs/bsapacker/src/GeneralArchiveBuilder.cpp new file mode 100644 index 0000000..aaf93c7 --- /dev/null +++ b/libs/bsapacker/src/GeneralArchiveBuilder.cpp @@ -0,0 +1,97 @@ +#include + +#include +#include +#include +#include + +using namespace libbsarch; + +namespace BsaPacker +{ + // 2 GiB limit. This does not consider size after compression or share data + const qint64 GeneralArchiveBuilder::SIZE_LIMIT = (qint64)1024 * 1024 * 1024 * 2; + + GeneralArchiveBuilder::GeneralArchiveBuilder(const IArchiveBuilderHelper* archiveBuilderHelper, const QDir& rootDir, const bsa_archive_type_t& type) + : m_ArchiveBuilderHelper(archiveBuilderHelper), m_RootDirectory(rootDir), m_ArchiveType(type) + { + this->m_Cancelled = false; + this->m_Archives.emplace_back(std::make_unique(this->m_ArchiveType)); + } + + uint32_t GeneralArchiveBuilder::setFiles() + { + uint32_t incompressibleFiles = 0; + uint32_t compressibleFiles = 0; + int count = 0; + qint64 currentSize = 0; + const auto& dirString = (this->m_RootDirectory.path() + '/').toStdWString(); + const auto& rootDirFiles = this->m_ArchiveBuilderHelper->getRootDirectoryFilenames(dirString); + qDebug() << "root is: " << m_RootDirectory.path() + '/'; + + QDirIterator iterator(this->m_RootDirectory.absolutePath(), QDir::Files, QDirIterator::Subdirectories); + while (iterator.hasNext()) { + QApplication::processEvents(); + + if (this->m_Cancelled) { + for (auto& archive : this->m_Archives) { + archive.reset(); + } + return 0; + } + + const QFileInfo& fileInfo = iterator.nextFileInfo(); + const QString& filepath = fileInfo.absoluteFilePath(); + const bool ignored = this->m_ArchiveBuilderHelper->isFileIgnorable(filepath.toStdWString(), rootDirFiles); + + Q_EMIT this->valueChanged(++count); + if (ignored) { + continue; + } + + currentSize += fileInfo.size(); + if (currentSize > SIZE_LIMIT) { + currentSize = fileInfo.size(); + this->m_Archives.back()->set_compressed(!static_cast(incompressibleFiles)); + incompressibleFiles = 0; + compressibleFiles = 0; + this->m_Archives.emplace_back(std::make_unique(this->m_ArchiveType)); + this->setShareData(true); + } + + this->m_ArchiveBuilderHelper->isIncompressible(filepath.toStdWString()) ? ++incompressibleFiles : ++compressibleFiles; + auto fileBlob = disk_blob( + dirString, + filepath.toStdWString()); + this->m_Archives.back()->add_file_from_disk(fileBlob); + qDebug() << "file is: " << filepath; + } + this->m_Archives.back()->set_compressed(!static_cast(incompressibleFiles)); + return incompressibleFiles + compressibleFiles; + } + + void GeneralArchiveBuilder::setShareData(const bool value) + { + this->m_Archives.back()->set_share_data(value); + } + + std::vector> GeneralArchiveBuilder::getArchives() + { + return std::move(this->m_Archives); + } + + uint32_t GeneralArchiveBuilder::getFileCount() const + { + return this->m_ArchiveBuilderHelper->getFileCount(this->m_RootDirectory.path().toStdWString()); + } + + QString GeneralArchiveBuilder::getRootPath() const + { + return this->m_RootDirectory.path(); + } + + void GeneralArchiveBuilder::cancel() + { + this->m_Cancelled = true; + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/HideLooseAssetService.cpp b/libs/bsapacker/src/HideLooseAssetService.cpp new file mode 100644 index 0000000..8342e56 --- /dev/null +++ b/libs/bsapacker/src/HideLooseAssetService.cpp @@ -0,0 +1,69 @@ +#include "HideLooseAssetService.h" + +#include +#include +#include +#include + +#include "SettingsService.h" + +namespace BsaPacker +{ + QString HideLooseAssetService::s_HiddenExt(".mohidden"); + + HideLooseAssetService::HideLooseAssetService(const ISettingsService* settingsService) + : m_SettingsService(settingsService) + { + } + + bool HideLooseAssetService::HideLooseAssets(const QDir& modDirectory) const + { + if (!this->m_SettingsService->GetPluginSetting(SettingsService::SETTING_HIDE_LOOSE_ASSETS).toBool()) { + return false; + } + + // Only hide loose files that were not excluded when creating the archive (blacklisted), so might still be needed by the mod + QStringList blacklistExtensions = this->m_SettingsService->GetPluginSetting(SettingsService::SETTING_BLACKLISTED_FILES).toString().split(';'); + for (auto& ext : blacklistExtensions) { + ext.prepend("*"); + } + + const QString& absModDir = modDirectory.absolutePath(); + for (const QString& subDir : modDirectory.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { + // Hide subdirectories + const QString& absPath = absModDir + '/' + subDir; + QDir originalDir(absPath); + if (originalDir.dirName().endsWith(s_HiddenExt) || originalDir.isEmpty()) { + continue; + } + if (!originalDir.rename(originalDir.absolutePath(), originalDir.absolutePath() + s_HiddenExt)) { + qWarning() << "Failed to hide " << originalDir.absolutePath(); + continue; + } + + // Restore files with blacklisted extension to their original directories + QDir hiddenDir(originalDir.absolutePath() + s_HiddenExt); + QDirIterator iterator(hiddenDir.absolutePath(), blacklistExtensions, QDir::Files, QDirIterator::Subdirectories); + while (iterator.hasNext()) { + QString hiddenFilePath = iterator.next(); + QString originalFilePath = originalDir.absoluteFilePath(hiddenDir.relativeFilePath(hiddenFilePath)); + originalDir.mkpath(originalFilePath.left(originalFilePath.lastIndexOf("/"))); + if (!originalDir.rename(hiddenFilePath, originalFilePath)) { + qWarning() << "Failed to unhide " << hiddenFilePath; + } + } + } + return true; + + /* + const std::function hideFolder = [&](const QString& subDir) + { + const QString& absPath = absModDir + '/' + subDir; + QDir dir(absPath); + if (!dir.dirName().endsWith(".mohidden")) + dir.rename(absPath, absPath + ".mohidden"); + }; + */ + //QtConcurrent::blockingMap(modDirectory.entryList(QDir::Dirs | QDir::NoDotAndDotDot), hideFolder); + } +} diff --git a/libs/bsapacker/src/HideLooseAssetService.h b/libs/bsapacker/src/HideLooseAssetService.h new file mode 100644 index 0000000..9acb60c --- /dev/null +++ b/libs/bsapacker/src/HideLooseAssetService.h @@ -0,0 +1,23 @@ +#ifndef HIDELOOSEASSETSERVICE_H +#define HIDELOOSEASSETSERVICE_H + +#include "bsapacker_global.h" +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT HideLooseAssetService : public IHideLooseAssetService + { + public: + HideLooseAssetService(const ISettingsService* settingsService); + bool HideLooseAssets(const QDir& modDirectory) const override; + + static QString s_HiddenExt; + + private: + const ISettingsService* m_SettingsService = nullptr; + }; +} + +#endif // HIDELOOSEASSETSERVICE_H diff --git a/libs/bsapacker/src/ModContext.cpp b/libs/bsapacker/src/ModContext.cpp new file mode 100644 index 0000000..896df9b --- /dev/null +++ b/libs/bsapacker/src/ModContext.cpp @@ -0,0 +1,66 @@ +#include "ModContext.h" + +#include +#include +#include "iplugingame.h" +#include "imodinterface.h" +#include "imodlist.h" +#include "NexusId.h" +#include +#include + +namespace BsaPacker +{ + const QStringList& ModContext::PLUGIN_TYPES = { "*.esm", "*.esp", "*.esl" }; + + ModContext::ModContext(MOBase::IOrganizer *moInfo) : m_Organizer(moInfo) + { + } + + QString ModContext::GetAbsoluteModPath(const QString &modName) const + { + const MOBase::IModInterface* const mod = m_Organizer->modList()->getMod(modName); + return mod->absolutePath(); + } + + int ModContext::GetNexusId() const + { + const MOBase::IPluginGame* managedGame = this->m_Organizer->managedGame(); + int nexusId = managedGame->nexusGameID(); + + // Games with no Nexus page have an ID of 0. Use primarySources in that case + if (nexusId != 0) { + return nexusId; + } + if (!managedGame->primarySources().isEmpty()) { + QString primarySource = managedGame->primarySources().first(); + if (primarySource == "FalloutNV") { // TTW + return NexusId::NewVegas; + } + } + return nexusId; + } + + QStringList ModContext::GetPlugins(const QDir& modDirectory) const + { + const int nexusId = this->GetNexusId(); + const QString extension = (nexusId == NexusId::Fallout4 || nexusId == NexusId::Starfield) ? ".ba2" : ".bsa"; + const std::function replace_extension = [&](QString fname) { + return fname.replace(fname.lastIndexOf('.'), 4, extension); + }; + + QStringList filenames = QtConcurrent::blockingMapped(modDirectory.entryList(PLUGIN_TYPES, QDir::Files), replace_extension); + filenames.removeDuplicates(); + return filenames << QStringLiteral("") + extension; + } + + QStringList ModContext::GetValidMods() const + { + const MOBase::IModList* const list = m_Organizer->modList(); + const std::function modStateValid = [&](const QString& mod) + { + return !mod.endsWith("_separator", Qt::CaseInsensitive) && (list->state(mod) & MOBase::IModList::STATE_VALID); + }; + return QtConcurrent::blockingFiltered(list->allMods(), modStateValid); + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/ModContext.h b/libs/bsapacker/src/ModContext.h new file mode 100644 index 0000000..43f57ef --- /dev/null +++ b/libs/bsapacker/src/ModContext.h @@ -0,0 +1,25 @@ +#ifndef MOD_CONTEXT_H +#define MOD_CONTEXT_H + +#include "bsapacker_global.h" +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT ModContext : public IModContext + { + public: + explicit ModContext(MOBase::IOrganizer* moInfo); + [[nodiscard]] QString GetAbsoluteModPath(const QString& modName) const override; + [[nodiscard]] int GetNexusId() const override; + [[nodiscard]] QStringList GetPlugins(const QDir& modDirectory) const override; + [[nodiscard]] QStringList GetValidMods() const override; + + private: + const MOBase::IOrganizer* m_Organizer = nullptr; + const static QStringList& PLUGIN_TYPES; + }; +} // namespace BsaPacker + +#endif // MOD_CONTEXT_H diff --git a/libs/bsapacker/src/ModDto.cpp b/libs/bsapacker/src/ModDto.cpp new file mode 100644 index 0000000..bf07dd9 --- /dev/null +++ b/libs/bsapacker/src/ModDto.cpp @@ -0,0 +1,43 @@ +#include "ModDto.h" + +#include + +namespace BsaPacker +{ + ModDto::ModDto( + const int nexusId, + const QString& modDir, + const QString& archiveName, + const QString& archiveExtension) + : m_NexusId(nexusId), + m_Path(modDir), + m_ArchiveName(archiveName), + m_ArchiveExtension(archiveExtension) + { + } + + QString ModDto::ArchiveExtension() const + { + return this->m_ArchiveExtension; + } + + QString ModDto::ArchiveName() const + { + return this->m_ArchiveName; + } + + QString ModDto::Directory() const + { + return this->m_Path.path(); + } + + QString ModDto::ModForename() const + { + return this->m_Path.dirName(); + } + + int ModDto::NexusId() const + { + return this->m_NexusId; + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/ModDto.h b/libs/bsapacker/src/ModDto.h new file mode 100644 index 0000000..9976065 --- /dev/null +++ b/libs/bsapacker/src/ModDto.h @@ -0,0 +1,29 @@ +#ifndef MODMETADTO_H +#define MODMETADTO_H + +#include "bsapacker_global.h" +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT ModDto : public IModDto + { + public: + ModDto(const int nexusId, const QString& modDir, const QString& archiveName, const QString& archiveExtension); + ~ModDto() override = default; + [[nodiscard]] QString ArchiveExtension() const override; + [[nodiscard]] QString ArchiveName() const override; + [[nodiscard]] QString Directory() const override; + [[nodiscard]] QString ModForename() const override; + [[nodiscard]] int NexusId() const override; + + private: + const int m_NexusId; + const QDir m_Path; + const QString m_ArchiveName; + const QString m_ArchiveExtension; + }; +} // namespace BsaPacker + +#endif // MODMETADTO_H diff --git a/libs/bsapacker/src/ModDtoFactory.cpp b/libs/bsapacker/src/ModDtoFactory.cpp new file mode 100644 index 0000000..3faed36 --- /dev/null +++ b/libs/bsapacker/src/ModDtoFactory.cpp @@ -0,0 +1,90 @@ +#include + +#include "ModDto.h" +#include "NullModDto.h" +#include "PackerDialog.h" +#include +#include + +namespace BsaPacker +{ + const uint16_t FALLOUT_4_NEXUS_ID = 1151; + const uint16_t STARFIELD_NEXUS_ID = 4187; + + ModDtoFactory::ModDtoFactory( + const IModContext* modContext, + IPackerDialog* packerDialog) : + m_ModContext(modContext), + m_PackerDialog(packerDialog) + { + } + + std::unique_ptr ModDtoFactory::Create() const + { + this->m_PackerDialog->RefreshModList(); + int result = m_PackerDialog->Exec(); + if (result != QDialog::DialogCode::Accepted) + { + return std::make_unique(); + } + + const int nexusId = this->m_ModContext->GetNexusId(); + const QString& modName = this->m_PackerDialog->SelectedMod(); + const QString& modDir = this->m_ModContext->GetAbsoluteModPath(modName); + const QString& pluginName = this->m_PackerDialog->SelectedPluginItem(); + const bool needsNewName = this->m_PackerDialog->IsNewFilename(); + const QString& archiveName = ModDtoFactory::ArchiveNameValidator(modName, pluginName, needsNewName); + if (archiveName == nullptr) { + return std::make_unique(); + } + + const QString& archiveExtension = (nexusId == FALLOUT_4_NEXUS_ID || nexusId == STARFIELD_NEXUS_ID) + ? QStringLiteral(".ba2") + : QStringLiteral(".bsa"); + + return std::make_unique(nexusId, modDir, archiveName, archiveExtension); + } + + QString ModDtoFactory::ArchiveNameValidator( + const QString& modName, + const QString& pluginName, + const bool needsNewName) + { + QString archive_name_base; + if (needsNewName) { + bool ok = false; + const QString& name = QInputDialog::getText(nullptr, + QStringLiteral("BSA Packer"), + QObject::tr("Archive name (no file extension):"), + QLineEdit::Normal, + modName, + &ok).simplified(); + if (!ok) { + return nullptr; + } + // Nameless plugins are not loaded in the games I tested. Nameless archives can be loaded in + // some games using the SArchiveList INI setting or equivalent. It is simpler to just prevent nameless archives + else if (name.isEmpty()) { + qWarning("Archive name cannot be empty. Cancelling archive creation."); + return nullptr; + } + archive_name_base = name; + } else { + archive_name_base = pluginName.chopped(4); // trims the file extension off + } + return archive_name_base; + } + + bool ModDtoFactory::CanOverwriteFile(const QString& filePath, + const QString& fileName) + { + const QString& absoluteFileName = filePath + '/' + fileName; + const QFileInfo fileInfo(absoluteFileName); + if (!fileInfo.exists() || !fileInfo.isFile()) { + return true; + } + + const QString& message = QObject::tr("File \"") + absoluteFileName + QObject::tr("\" already exists. Overwrite?"); + return QMessageBox::question(nullptr, QStringLiteral("BSA Packer"), message, QMessageBox::Ok | QMessageBox::Cancel) != QMessageBox::Cancel; + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/NewVegasDummyPluginService.cpp b/libs/bsapacker/src/NewVegasDummyPluginService.cpp new file mode 100644 index 0000000..d48f4b3 --- /dev/null +++ b/libs/bsapacker/src/NewVegasDummyPluginService.cpp @@ -0,0 +1,28 @@ +#include "NewVegasDummyPluginService.h" + +#include + +namespace BsaPacker +{ + NewVegasDummyPluginService::NewVegasDummyPluginService( + const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic) + : m_FileWriterService(fileWriterService), + m_DummyPluginLogic(dummyPluginLogic) + { + } + + bool NewVegasDummyPluginService::CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const + { + const QString& absoluteFileNameNoExtension = modPath + '/' + archiveNameBase; + if (!this->m_DummyPluginLogic->canCreateDummyESP(absoluteFileNameNoExtension, bsa_archive_type_e::baFO3)) + { + return false; + } + const std::string& absoluteFileName = absoluteFileNameNoExtension.toStdString() + ".esp"; + return this->m_FileWriterService->Write(absoluteFileName, + reinterpret_cast(NewVegasDummyPluginService::RAW_NEWVEGAS), + sizeof(NewVegasDummyPluginService::RAW_NEWVEGAS)); + } +} diff --git a/libs/bsapacker/src/NewVegasDummyPluginService.h b/libs/bsapacker/src/NewVegasDummyPluginService.h new file mode 100644 index 0000000..69c6be9 --- /dev/null +++ b/libs/bsapacker/src/NewVegasDummyPluginService.h @@ -0,0 +1,42 @@ +#ifndef NEWVEGASDUMMYPLUGINSERVICE_H +#define NEWVEGASDUMMYPLUGINSERVICE_H + +#include "bsapacker_global.h" +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT NewVegasDummyPluginService : public IDummyPluginService + { + public: + NewVegasDummyPluginService(const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic); + ~NewVegasDummyPluginService() override = default; + bool CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const override; + + static constexpr unsigned char RAW_NEWVEGAS[] = { + 0x54, 0x45, 0x53, 0x34, 0x42, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0x48, 0x45, 0x44, 0x52, 0x0C, 0x00, 0x1F, 0x85, + 0xAB, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xDD, 0x0A, + 0x00, 0x00, 0x43, 0x4E, 0x41, 0x4D, 0x08, 0x00, + 0x44, 0x45, 0x46, 0x41, 0x55, 0x4C, 0x54, 0x00, + 0x4D, 0x41, 0x53, 0x54, 0x0E, 0x00, 0x46, 0x61, + 0x6C, 0x6C, 0x6F, 0x75, 0x74, 0x4E, 0x56, 0x2E, + 0x65, 0x73, 0x6D, 0x00, 0x44, 0x41, 0x54, 0x41, + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + }; + + private: + const IFileWriterService* m_FileWriterService = nullptr; + const IDummyPluginLogic* m_DummyPluginLogic = nullptr; + }; +} + + +#endif // NEWVEGASDUMMYPLUGINSERVICE_H diff --git a/libs/bsapacker/src/NexusId.h b/libs/bsapacker/src/NexusId.h new file mode 100644 index 0000000..c1971d9 --- /dev/null +++ b/libs/bsapacker/src/NexusId.h @@ -0,0 +1,22 @@ +#ifndef NEXUSID_H +#define NEXUSID_H + +namespace BsaPacker +{ + enum NexusId + { + // Skyrim VR, Fallout 4 VR, and TTW don't have Nexus pages so are 0 + Morrowind = 100, + Oblivion = 101, + Fallout3 = 120, + NewVegas = 130, + Skyrim = 110, + SkyrimSE = 1704, + Fallout4 = 1151, + Enderal = 2736, + EnderalSE = 3685, + Starfield = 4187 + }; +} // namespace BsaPacker + +#endif // NEXUSID_H diff --git a/libs/bsapacker/src/NullArchiveBuilder.cpp b/libs/bsapacker/src/NullArchiveBuilder.cpp new file mode 100644 index 0000000..ce2fd8d --- /dev/null +++ b/libs/bsapacker/src/NullArchiveBuilder.cpp @@ -0,0 +1,32 @@ +#include + +namespace BsaPacker +{ + uint32_t NullArchiveBuilder::setFiles() + { + return 0; + } + + void NullArchiveBuilder::setShareData(const bool) + { + } + + std::vector> NullArchiveBuilder::getArchives() + { + return {}; + } + + uint32_t NullArchiveBuilder::getFileCount() const + { + return 0; + } + + QString NullArchiveBuilder::getRootPath() const + { + return QString(); + } + + void NullArchiveBuilder::cancel() + { + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/NullDummyPluginService.cpp b/libs/bsapacker/src/NullDummyPluginService.cpp new file mode 100644 index 0000000..52d8609 --- /dev/null +++ b/libs/bsapacker/src/NullDummyPluginService.cpp @@ -0,0 +1,9 @@ +#include "NullDummyPluginService.h" + +namespace BsaPacker +{ + bool NullDummyPluginService::CreatePlugin([[maybe_unused]] const QString& modPath, [[maybe_unused]] const QString& archiveNameBase) const + { + return false; + } +} diff --git a/libs/bsapacker/src/NullDummyPluginService.h b/libs/bsapacker/src/NullDummyPluginService.h new file mode 100644 index 0000000..c67af02 --- /dev/null +++ b/libs/bsapacker/src/NullDummyPluginService.h @@ -0,0 +1,17 @@ +#ifndef NULLDUMMYPLUGINSERVICE_H +#define NULLDUMMYPLUGINSERVICE_H + +#include + +namespace BsaPacker +{ + class NullDummyPluginService : public IDummyPluginService + { + public: + NullDummyPluginService() = default; + ~NullDummyPluginService() override = default; + bool CreatePlugin(const QString& modPath, const QString& archiveNameBase) const override; + }; +} + +#endif // NULLDUMMYPLUGINSERVICE_H diff --git a/libs/bsapacker/src/NullModDto.cpp b/libs/bsapacker/src/NullModDto.cpp new file mode 100644 index 0000000..255219b --- /dev/null +++ b/libs/bsapacker/src/NullModDto.cpp @@ -0,0 +1,29 @@ +#include "NullModDto.h" + +namespace BsaPacker +{ + QString NullModDto::ArchiveExtension() const + { + return QString(); + } + + QString NullModDto::ArchiveName() const + { + return QString(); + } + + QString NullModDto::Directory() const + { + return QString(); + } + + QString NullModDto::ModForename() const + { + return QString(); + } + + int NullModDto::NexusId() const + { + return 0; + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/NullModDto.h b/libs/bsapacker/src/NullModDto.h new file mode 100644 index 0000000..841e792 --- /dev/null +++ b/libs/bsapacker/src/NullModDto.h @@ -0,0 +1,20 @@ +#ifndef NULLMODDTO_H +#define NULLMODDTO_H + +#include +#include + +namespace BsaPacker +{ + class NullModDto : public IModDto + { + public: + [[nodiscard]] QString ArchiveExtension() const override; + [[nodiscard]] QString ArchiveName() const override; + [[nodiscard]] QString Directory() const override; + [[nodiscard]] QString ModForename() const override; + [[nodiscard]] int NexusId() const override; + }; +} // namespace BsaPacker + +#endif // NULLMODDTO_H diff --git a/libs/bsapacker/src/OblivionDummyPluginService.cpp b/libs/bsapacker/src/OblivionDummyPluginService.cpp new file mode 100644 index 0000000..2f771f3 --- /dev/null +++ b/libs/bsapacker/src/OblivionDummyPluginService.cpp @@ -0,0 +1,26 @@ +#include "OblivionDummyPluginService.h" + +namespace BsaPacker +{ + OblivionDummyPluginService::OblivionDummyPluginService( + const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic) + : m_FileWriterService(fileWriterService), + m_DummyPluginLogic(dummyPluginLogic) + { + } + + bool BsaPacker::OblivionDummyPluginService::CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const + { + const QString& fileNameNoExtension = modPath + '/' + archiveNameBase; + if (!this->m_DummyPluginLogic->canCreateDummyESP(fileNameNoExtension, bsa_archive_type_e::baTES4)) + { + return false; + } + const std::string& absoluteFileName = fileNameNoExtension.toStdString() + ".esp"; + return this->m_FileWriterService->Write(absoluteFileName, + reinterpret_cast(OblivionDummyPluginService::RAW_OBLIVION), + sizeof(OblivionDummyPluginService::RAW_OBLIVION)); + } +} diff --git a/libs/bsapacker/src/OblivionDummyPluginService.h b/libs/bsapacker/src/OblivionDummyPluginService.h new file mode 100644 index 0000000..7c7b40a --- /dev/null +++ b/libs/bsapacker/src/OblivionDummyPluginService.h @@ -0,0 +1,34 @@ +#ifndef OBLIVIONDUMMYPLUGINSERVICE_H +#define OBLIVIONDUMMYPLUGINSERVICE_H + +#include "bsapacker_global.h" +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT OblivionDummyPluginService : public IDummyPluginService + { + public: + OblivionDummyPluginService(const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic); + ~OblivionDummyPluginService() override = default; + bool CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const override; + + static constexpr unsigned char RAW_OBLIVION[] = { + 0x54, 0x45, 0x53, 0x34, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x45, 0x44, 0x52, + 0x0C, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x43, 0x4E, 0x41, 0x4D, 0x08, 0x00, 0x44, 0x45, 0x46, 0x41, + 0x55, 0x4C, 0x54, 0x00 + }; + + private: + const IFileWriterService* m_FileWriterService = nullptr; + const IDummyPluginLogic* m_DummyPluginLogic = nullptr; + }; +} + +#endif // OBLIVIONDUMMYPLUGINSERVICE_H diff --git a/libs/bsapacker/src/OverrideFileService.cpp b/libs/bsapacker/src/OverrideFileService.cpp new file mode 100644 index 0000000..3e4ebec --- /dev/null +++ b/libs/bsapacker/src/OverrideFileService.cpp @@ -0,0 +1,44 @@ +#include "OverrideFileService.h" + +#include +#include +#include + +namespace BsaPacker { + const uint16_t FALLOUT_3_NEXUS_ID = 120; + const uint16_t NEW_VEGAS_NEXUS_ID = 130; + + OverrideFileService::OverrideFileService( + const IFileWriterService* fileWriterService) + : m_FileWriterService(fileWriterService) + { + } + + // TODO: Add detection for Command Extender and JIP LN NVSE and warn if missing + bool OverrideFileService::CreateOverrideFile(const int nexusId, + const QString& modPath, + const QStringList& archiveNames) const { + + if (nexusId != FALLOUT_3_NEXUS_ID && nexusId != NEW_VEGAS_NEXUS_ID) { + return false; + } + + if (MOBase::QuestionBoxMemory::query(QApplication::activeModalWidget(), "BSAPacker", "Create .override file?", + "Do you want to create an override file for the archive(s)?", + QDialogButtonBox::No | QDialogButtonBox::Yes, QDialogButtonBox::No) & QDialogButtonBox::No) { + return false; + } + + bool res = true; + for (const QString& baseName : archiveNames) { + const QString& fileNameNoExtension = modPath + '/' + baseName; + const std::string& absoluteFileName = fileNameNoExtension.toStdString() + ".override"; + if (!this->m_FileWriterService->Write(absoluteFileName, nullptr, 0)) { + qWarning() << "Failed to create" << absoluteFileName; + res = false; + } + } + + return res; + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/OverrideFileService.h b/libs/bsapacker/src/OverrideFileService.h new file mode 100644 index 0000000..ffcad17 --- /dev/null +++ b/libs/bsapacker/src/OverrideFileService.h @@ -0,0 +1,21 @@ +#ifndef OVERRIDEFILESERVICE_H +#define OVERRIDEFILESERVICE_H + +#include "bsapacker_global.h" +#include +#include + +namespace BsaPacker { + class BSAPACKER_EXPORT OverrideFileService : public IOverrideFileService { + public: + OverrideFileService(const IFileWriterService* fileWriterService); + bool CreateOverrideFile(const int nexusId, + const QString& modPath, + const QStringList& archiveNames) const override; + + private: + const IFileWriterService* m_FileWriterService = nullptr; + }; +} // namespace BsaPacker + +#endif // OVERRIDEFILESERVICE_H diff --git a/libs/bsapacker/src/PackerDialog.cpp b/libs/bsapacker/src/PackerDialog.cpp new file mode 100644 index 0000000..cb6bd33 --- /dev/null +++ b/libs/bsapacker/src/PackerDialog.cpp @@ -0,0 +1,98 @@ +#include "PackerDialog.h" + +#include +#include +#include +#include + +namespace BsaPacker +{ + constexpr uint32_t DIALOG_WIDTH = 240; + constexpr uint32_t DIALOG_HEIGHT = 200; + constexpr uint32_t COMBO_MIN_WIDTH = 100; + constexpr uint32_t LIST_MIN_HEIGHT = 100; + + PackerDialog::PackerDialog(const IModContext* modContext) : + m_ModContext(modContext), + buttonsOkCancelMod(QDialogButtonBox::Ok | QDialogButtonBox::Cancel) + { + QPushButton* ok = this->buttonsOkCancelMod.button(QDialogButtonBox::Ok); + ok->setEnabled(false); + + this->labelChooseMod.setText(QObject::tr("Choose a mod to pack:")); + this->labelChooseName.setText(QObject::tr("Choose a name for the packed archive:")); + + this->resize(DIALOG_WIDTH, DIALOG_HEIGHT); + this->setWindowTitle(QObject::tr("BSA Packer")); + + this->comboModList.setMinimumWidth(COMBO_MIN_WIDTH); + + this->listArchiveNames.setMinimumHeight(LIST_MIN_HEIGHT); + + this->layoutHorizontal.addStretch(1); + this->layoutHorizontal.addWidget(&buttonsOkCancelMod); + + this->layoutVertical.addWidget(&labelChooseMod); + this->layoutVertical.addWidget(&comboModList); + this->layoutVertical.addWidget(&labelChooseName); + this->layoutVertical.addWidget(&listArchiveNames); + this->layoutVertical.addLayout(&layoutHorizontal); + + this->setLayout(&layoutVertical); + + QObject::connect(&listArchiveNames, &QListWidget::itemSelectionChanged, this, &PackerDialog::RefreshOkButton, Qt::QueuedConnection); + QObject::connect(&listArchiveNames, qOverload(&QListWidget::itemDoubleClicked), this, &QDialog::accept); + QObject::connect(&buttonsOkCancelMod, &QDialogButtonBox::accepted, this, &QDialog::accept); + QObject::connect(&buttonsOkCancelMod, &QDialogButtonBox::rejected, this, &QDialog::reject); + QObject::connect(&comboModList, qOverload(&QComboBox::currentTextChanged), [this](auto&& text) { this->UpdateNameList(text); }); + } + + bool PackerDialog::IsNewFilename() const + { + const int currentIndex = this->listArchiveNames.currentIndex().row() + 1; + const int count = this->listArchiveNames.count(); + return currentIndex == count; + } + + QString PackerDialog::SelectedMod() const + { + return this->comboModList.currentText(); + } + + QString PackerDialog::SelectedPluginItem() const + { + const QListWidgetItem* const currentItem = this->listArchiveNames.currentItem(); + return currentItem != nullptr ? currentItem->text() : QString(); + } + + void PackerDialog::RefreshModList() + { + const QStringList validMods = this->m_ModContext->GetValidMods(); + this->comboModList.clear(); + this->comboModList.addItems(validMods); + } + + void PackerDialog::UpdateNameList(const QString& modName) + { + this->listArchiveNames.clear(); + const QString& modPath = this->m_ModContext->GetAbsoluteModPath(modName); + const QStringList& filenames = this->m_ModContext->GetPlugins(modPath); + this->listArchiveNames.addItems(filenames); + } + + int PackerDialog::Exec() + { + return QDialog::exec(); + } + + void PackerDialog::RefreshSelectedName() + { + Q_EMIT this->comboModList.currentTextChanged(this->comboModList.currentText()); + } + + void PackerDialog::RefreshOkButton() + { + QPushButton* ok = this->buttonsOkCancelMod.button(QDialogButtonBox::Ok); + ok->setEnabled(this->listArchiveNames.currentItem() != nullptr); + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/PackerDialog.h b/libs/bsapacker/src/PackerDialog.h new file mode 100644 index 0000000..843e842 --- /dev/null +++ b/libs/bsapacker/src/PackerDialog.h @@ -0,0 +1,48 @@ +#ifndef PACKERDIALOG_H +#define PACKERDIALOG_H + +#include +#include + +#include +#include +#include +#include +#include + + +namespace BsaPacker +{ + class PackerDialog : public QDialog, public IPackerDialog + { + Q_OBJECT + public: + explicit PackerDialog(const IModContext* modContext); + ~PackerDialog() override = default; + + // IPackerDialog interface + [[nodiscard]] bool IsNewFilename() const override; + void RefreshModList() override; + [[nodiscard]] QString SelectedMod() const override; + [[nodiscard]] QString SelectedPluginItem() const override; + void UpdateNameList(const QString&) override; + void RefreshSelectedName() override; + int Exec() override; + + public Q_SLOTS: + void RefreshOkButton() override; + + private: + const IModContext* m_ModContext = nullptr; + + QComboBox comboModList; + QLabel labelChooseMod; + QLabel labelChooseName; + QListWidget listArchiveNames; + QVBoxLayout layoutVertical; + QHBoxLayout layoutHorizontal; + QDialogButtonBox buttonsOkCancelMod; + }; +} // namespace BsaPacker + +#endif // PACKERDIALOG_H diff --git a/libs/bsapacker/src/PackerDialogFactory.cpp b/libs/bsapacker/src/PackerDialogFactory.cpp new file mode 100644 index 0000000..1face6a --- /dev/null +++ b/libs/bsapacker/src/PackerDialogFactory.cpp @@ -0,0 +1,5 @@ +#include "PackerDialogFactory.h" + +namespace BsaPacker +{ +} diff --git a/libs/bsapacker/src/PackerDialogFactory.h b/libs/bsapacker/src/PackerDialogFactory.h new file mode 100644 index 0000000..fcc03d8 --- /dev/null +++ b/libs/bsapacker/src/PackerDialogFactory.h @@ -0,0 +1,16 @@ +#ifndef PACKERDIALOGFACTORY_H +#define PACKERDIALOGFACTORY_H + +#include + +namespace BsaPacker +{ + class PackerDialogFactory : public IPackerDialogFactory + { + public: + ~PackerDialogFactory() override = default; + std::unique_ptr Create() const override; + }; +} + +#endif // PACKERDIALOGFACTORY_H diff --git a/libs/bsapacker/src/SettingsService.cpp b/libs/bsapacker/src/SettingsService.cpp new file mode 100644 index 0000000..6ad8839 --- /dev/null +++ b/libs/bsapacker/src/SettingsService.cpp @@ -0,0 +1,28 @@ +#include "SettingsService.h" + +namespace BsaPacker +{ + const QString& SettingsService::SETTING_HIDE_LOOSE_ASSETS = QStringLiteral("hide_loose_assets"); + const QString& SettingsService::SETTING_CREATE_PLUGINS = QStringLiteral("create_plugins"); + const QString& SettingsService::SETTING_BLACKLISTED_FILES = QStringLiteral("blacklisted_files"); + const QString& SettingsService::SETTING_COMPRESS_ARCHIVES = QStringLiteral("compress_archives"); + //const QString& SettingsService::SETTING_SPLIT_ARCHIVES = "split_archives"; + + const QList& SettingsService::PluginSettings = { + MOBase::PluginSetting(SettingsService::SETTING_HIDE_LOOSE_ASSETS, QObject::tr("After creating the archive, set loose assets to hidden."), false), + MOBase::PluginSetting(SettingsService::SETTING_CREATE_PLUGINS, QObject::tr("Create a dummy plugin to load the archive if one does not exist."), false), + MOBase::PluginSetting(SettingsService::SETTING_BLACKLISTED_FILES, QObject::tr("Specify a semi-colon separated list of file extensions to ignore when packing."), ".txt;.hkx;.xml;.ini;.bk2"), + MOBase::PluginSetting(SettingsService::SETTING_COMPRESS_ARCHIVES, QObject::tr("Compress archives if they do not contain incompressible files. Texture archives for Fallout 4 and Starfield will always be compressed. Morrowind archives will never be compressed."), true) + //MOBase::PluginSetting(SettingsService::SETTING_SPLIT_ARCHIVES, QObject::tr("Automatically create multiple archives if they exceed the size limit."), false); + }; + + SettingsService::SettingsService(MOBase::IOrganizer* organizer) + : m_Organizer(organizer) + { + } + + QVariant SettingsService::GetPluginSetting(const QString& setting) const + { + return this->m_Organizer->pluginSetting(QStringLiteral("BSA Packer"), setting); + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/SettingsService.h b/libs/bsapacker/src/SettingsService.h new file mode 100644 index 0000000..42f9299 --- /dev/null +++ b/libs/bsapacker/src/SettingsService.h @@ -0,0 +1,32 @@ +#ifndef SETTINGSSERVICE_H +#define SETTINGSSERVICE_H + +#include + +#include +#include + +namespace BsaPacker +{ + class SettingsService : public ISettingsService + { + public: + explicit SettingsService(MOBase::IOrganizer* organizer); + ~SettingsService() override = default; + [[nodiscard]] QVariant GetPluginSetting(const QString& setting) const override; + + static const QString& SETTING_ENABLED; + static const QString& SETTING_HIDE_LOOSE_ASSETS; + static const QString& SETTING_CREATE_PLUGINS; + static const QString& SETTING_BLACKLISTED_FILES; + static const QString& SETTING_COMPRESS_ARCHIVES; + //static const QString& SETTING_SPLIT_ARCHIVES; + + static const QList& PluginSettings; + + private: + MOBase::IOrganizer* m_Organizer = nullptr; + }; +} // namespace BsaPacker + +#endif // SETTINGSSERVICE_H diff --git a/libs/bsapacker/src/SkyrimDummyPluginService.cpp b/libs/bsapacker/src/SkyrimDummyPluginService.cpp new file mode 100644 index 0000000..81c606e --- /dev/null +++ b/libs/bsapacker/src/SkyrimDummyPluginService.cpp @@ -0,0 +1,26 @@ +#include "SkyrimDummyPluginService.h" + +namespace BsaPacker +{ + SkyrimDummyPluginService::SkyrimDummyPluginService( + const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic) + : m_FileWriterService(fileWriterService), + m_DummyPluginLogic(dummyPluginLogic) + { + } + + bool SkyrimDummyPluginService::CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const + { + const QString& fileNameNoExtension = modPath + '/' + archiveNameBase; + if (!this->m_DummyPluginLogic->canCreateDummyESP(fileNameNoExtension, bsa_archive_type_e::baFO3)) + { + return false; + } + const std::string& absoluteFileName = fileNameNoExtension.toStdString() + ".esp"; + return this->m_FileWriterService->Write(absoluteFileName, + reinterpret_cast(SkyrimDummyPluginService::RAW_SKYRIM), + sizeof(SkyrimDummyPluginService::RAW_SKYRIM)); + } +} diff --git a/libs/bsapacker/src/SkyrimDummyPluginService.h b/libs/bsapacker/src/SkyrimDummyPluginService.h new file mode 100644 index 0000000..2866ef8 --- /dev/null +++ b/libs/bsapacker/src/SkyrimDummyPluginService.h @@ -0,0 +1,34 @@ +#ifndef SKYRIMDUMMYPLUGINSERVICE_H +#define SKYRIMDUMMYPLUGINSERVICE_H + +#include "bsapacker_global.h" +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT SkyrimDummyPluginService : public IDummyPluginService + { + public: + SkyrimDummyPluginService(const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic); + ~SkyrimDummyPluginService() override = default; + [[nodiscard]] bool CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const override; + + static constexpr unsigned char RAW_SKYRIM[] = { + 0x54, 0x45, 0x53, 0x34, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2B, 0x00, 0x00, 0x00, + 0x48, 0x45, 0x44, 0x52, 0x0C, 0x00, 0x9A, 0x99, 0xD9, 0x3F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x43, 0x4E, 0x41, 0x4D, 0x01, 0x00, + 0x00 + }; + + private: + const IFileWriterService* m_FileWriterService = nullptr; + const IDummyPluginLogic* m_DummyPluginLogic = nullptr; + }; +} + +#endif // SKYRIMDUMMYPLUGINSERVICE_H diff --git a/libs/bsapacker/src/SkyrimSEDummyPluginService.cpp b/libs/bsapacker/src/SkyrimSEDummyPluginService.cpp new file mode 100644 index 0000000..4aa4b7a --- /dev/null +++ b/libs/bsapacker/src/SkyrimSEDummyPluginService.cpp @@ -0,0 +1,26 @@ +#include "SkyrimSEDummyPluginService.h" + +namespace BsaPacker +{ + SkyrimSEDummyPluginService::SkyrimSEDummyPluginService( + const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic) + : m_FileWriterService(fileWriterService), + m_DummyPluginLogic(dummyPluginLogic) + { + } + + bool SkyrimSEDummyPluginService::CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const + { + const QString& fileNameNoExtension = modPath + '/' + archiveNameBase; + if (!this->m_DummyPluginLogic->canCreateDummyESP(fileNameNoExtension, bsa_archive_type_e::baSSE)) + { + return false; + } + const std::string& absoluteFileName = fileNameNoExtension.toStdString() + ".esp"; + return this->m_FileWriterService->Write(absoluteFileName, + reinterpret_cast(SkyrimSEDummyPluginService::RAW_SKYRIMSE), + sizeof(SkyrimSEDummyPluginService::RAW_SKYRIMSE)); + } +} diff --git a/libs/bsapacker/src/SkyrimSEDummyPluginService.h b/libs/bsapacker/src/SkyrimSEDummyPluginService.h new file mode 100644 index 0000000..dc348a8 --- /dev/null +++ b/libs/bsapacker/src/SkyrimSEDummyPluginService.h @@ -0,0 +1,35 @@ +#ifndef SKYRIMSEDUMMYPLUGINSERVICE_H +#define SKYRIMSEDUMMYPLUGINSERVICE_H + +#include "bsapacker_global.h" +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT SkyrimSEDummyPluginService : public IDummyPluginService + { + public: + SkyrimSEDummyPluginService(const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic); + ~SkyrimSEDummyPluginService() override = default; + bool CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const override; + + static constexpr unsigned char RAW_SKYRIMSE[] = { + 0x54, 0x45, 0x53, 0x34, 0x19, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, + 0x48, 0x45, 0x44, 0x52, 0x0C, 0x00, 0x9A, 0x99, 0xD9, 0x3F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x43, 0x4E, 0x41, 0x4D, 0x01, 0x00, + 0x00 + }; + + private: + const IFileWriterService* m_FileWriterService = nullptr; + const IDummyPluginLogic* m_DummyPluginLogic = nullptr; + }; +} + + +#endif // SKYRIMSEDUMMYPLUGINSERVICE_H diff --git a/libs/bsapacker/src/StarfieldDummyPluginService.cpp b/libs/bsapacker/src/StarfieldDummyPluginService.cpp new file mode 100644 index 0000000..ca3e8e4 --- /dev/null +++ b/libs/bsapacker/src/StarfieldDummyPluginService.cpp @@ -0,0 +1,26 @@ +#include "StarfieldDummyPluginService.h" + +namespace BsaPacker +{ + StarfieldDummyPluginService::StarfieldDummyPluginService( + const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic) + : m_FileWriterService(fileWriterService), + m_DummyPluginLogic(dummyPluginLogic) + { + } + + bool StarfieldDummyPluginService::CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const + { + const QString& fileNameNoExtension = modPath + '/' + archiveNameBase; + if (!this->m_DummyPluginLogic->canCreateDummyESL(fileNameNoExtension, bsa_archive_type_e::baSF) && !this->m_DummyPluginLogic->canCreateDummyESL(fileNameNoExtension, bsa_archive_type_e::baSFdds)) + { + return false; + } + const std::string& absoluteFileName = fileNameNoExtension.toStdString() + ".esm"; + return this->m_FileWriterService->Write(absoluteFileName, + reinterpret_cast(StarfieldDummyPluginService::RAW_STARFIELD), + sizeof(StarfieldDummyPluginService::RAW_STARFIELD)); + } +} diff --git a/libs/bsapacker/src/StarfieldDummyPluginService.h b/libs/bsapacker/src/StarfieldDummyPluginService.h new file mode 100644 index 0000000..a7776f3 --- /dev/null +++ b/libs/bsapacker/src/StarfieldDummyPluginService.h @@ -0,0 +1,34 @@ +#ifndef STARFIELDDUMMYPLUGINSERVICE_H +#define STARFIELDDUMMYPLUGINSERVICE_H + +#include "bsapacker_global.h" +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT StarfieldDummyPluginService : public IDummyPluginService + { + public: + StarfieldDummyPluginService(const IFileWriterService* fileWriterService, + const IDummyPluginLogic* dummyPluginLogic); + ~StarfieldDummyPluginService() override = default; + bool CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const override; + + static constexpr unsigned char RAW_STARFIELD[] = { + 0x54, 0x45, 0x53, 0x34, 0x19, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2B, 0x02, 0x00, 0x00, + 0x48, 0x45, 0x44, 0x52, 0x0C, 0x00, 0x8F, 0xC2, 0x75, 0x3F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x43, 0x4E, 0x41, 0x4D, 0x01, 0x00, + 0x00 + }; + + private: + const IFileWriterService* m_FileWriterService = nullptr; + const IDummyPluginLogic* m_DummyPluginLogic = nullptr; + }; +} + +#endif // STARFIELDDUMMYPLUGINSERVICE_H diff --git a/libs/bsapacker/src/TextureArchiveBuilder.cpp b/libs/bsapacker/src/TextureArchiveBuilder.cpp new file mode 100644 index 0000000..123a48d --- /dev/null +++ b/libs/bsapacker/src/TextureArchiveBuilder.cpp @@ -0,0 +1,137 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +using namespace libbsarch; + +namespace BsaPacker +{ + // 4 GiB limit for the Fallout 4 Creation Kit. The game itself has no limit so could make an optional setting + // This does not consider size after compression or share data + const qint64 TextureArchiveBuilder::SIZE_LIMIT = (qint64)1024 * 1024 * 1024 * 4; + + TextureArchiveBuilder::TextureArchiveBuilder(const IArchiveBuilderHelper* archiveBuilderHelper, const QDir& rootDir, const bsa_archive_type_t& type) + : m_ArchiveBuilderHelper(archiveBuilderHelper), m_RootDirectory(rootDir), m_ArchiveType(type) + { + this->m_Cancelled = false; + this->m_Archives.emplace_back(std::make_unique(this->m_ArchiveType)); + } + + uint32_t TextureArchiveBuilder::setFiles() + { + uint32_t compressibleFiles = 0; + int count = 0; + qint64 currentSize = 0; + const auto& dirString = (this->m_RootDirectory.path() + '/').toStdWString(); + const auto& rootDirFiles = this->m_ArchiveBuilderHelper->getRootDirectoryFilenames(dirString); + qDebug() << "root is: " << m_RootDirectory.path() + '/'; + + QDirIterator iterator(this->m_RootDirectory.absolutePath(), QDir::Files, QDirIterator::Subdirectories); + while (iterator.hasNext()) { + QApplication::processEvents(); + + if (this->m_Cancelled) { + for (auto& archive : this->m_Archives) { + archive.reset(); + } + return 0; + } + + const QFileInfo& fileInfo = iterator.nextFileInfo(); + const QString& filepath = fileInfo.absoluteFilePath(); + const bool ignored = this->m_ArchiveBuilderHelper->isFileIgnorable(filepath.toStdWString(), rootDirFiles); + + Q_EMIT this->valueChanged(++count); + if (ignored || !filepath.endsWith(".dds", Qt::CaseInsensitive)) { + continue; + } + + currentSize += fileInfo.size(); + if (currentSize > SIZE_LIMIT) { + currentSize = fileInfo.size(); + this->m_Archives.back()->set_compressed(true); + this->m_Archives.back()->set_dds_callback(TextureArchiveBuilder::DDSCallback, this->getRootPath().toStdWString()); + compressibleFiles = 0; + this->m_Archives.emplace_back(std::make_unique(this->m_ArchiveType)); + this->setShareData(true); + } + + ++compressibleFiles; + auto fileBlob = disk_blob( + dirString, + filepath.toStdWString()); + this->m_Archives.back()->add_file_from_disk(fileBlob); + qDebug() << "file is: " << filepath; + } + this->m_Archives.back()->set_compressed(true); + this->m_Archives.back()->set_dds_callback(TextureArchiveBuilder::DDSCallback, this->getRootPath().toStdWString()); + return compressibleFiles; + } + + void TextureArchiveBuilder::setShareData(const bool value) + { + this->m_Archives.back()->set_share_data(value); + } + + std::vector> TextureArchiveBuilder::getArchives() + { + return std::move(this->m_Archives); + } + + uint32_t TextureArchiveBuilder::getFileCount() const + { + return this->m_ArchiveBuilderHelper->getFileCount(this->m_RootDirectory.path().toStdWString()); + } + + QString TextureArchiveBuilder::getRootPath() const + { + return this->m_RootDirectory.path(); + } + + void TextureArchiveBuilder::cancel() + { + this->m_Cancelled = true; + } + + void TextureArchiveBuilder::DDSCallback(bsa_archive_t, const wchar_t* file_path, bsa_dds_info_t* dds_info, void* context) + { + const auto& path = *static_cast(context) + L'/' + std::wstring(file_path); + std::ifstream file(std::filesystem::path(path), std::ios::binary); + if (!file) { + throw std::runtime_error("Failed to open DDS"); + } + + uint8_t header[128] = {}; + file.read(reinterpret_cast(header), sizeof(header)); + if (file.gcount() != static_cast(sizeof(header))) { + throw std::runtime_error("Invalid DDS header"); + } + + if (!(header[0] == 'D' && header[1] == 'D' && header[2] == 'S' && header[3] == ' ')) { + throw std::runtime_error("Not a DDS file"); + } + + auto le32 = [](const uint8_t* p) -> uint32_t { + return static_cast(p[0]) | + (static_cast(p[1]) << 8) | + (static_cast(p[2]) << 16) | + (static_cast(p[3]) << 24); + }; + + // DDS_HEADER starts at byte 4. + const uint32_t height = le32(&header[12]); // 4 + 8 + const uint32_t width = le32(&header[16]); // 4 + 12 + const uint32_t mips = le32(&header[28]); // 4 + 24 + + dds_info->width = width; + dds_info->height = height; + dds_info->mipmaps = mips > 0 ? mips : 1; + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/TexturelessArchiveBuilder.cpp b/libs/bsapacker/src/TexturelessArchiveBuilder.cpp new file mode 100644 index 0000000..586a489 --- /dev/null +++ b/libs/bsapacker/src/TexturelessArchiveBuilder.cpp @@ -0,0 +1,98 @@ +#include + +#include +#include +#include +#include + +using namespace libbsarch; + +namespace BsaPacker +{ + // 4 GiB limit for the Fallout 4 Creation Kit. The game itself has no limit so could make an optional setting + // This does not consider size after compression or share data + const qint64 TexturelessArchiveBuilder::SIZE_LIMIT = (qint64)1024 * 1024 * 1024 * 4; + + TexturelessArchiveBuilder::TexturelessArchiveBuilder(const IArchiveBuilderHelper* archiveBuilderHelper, const QDir& rootDir, const bsa_archive_type_t& type) + : m_ArchiveBuilderHelper(archiveBuilderHelper), m_RootDirectory(rootDir), m_ArchiveType(type) + { + this->m_Cancelled = false; + this->m_Archives.emplace_back(std::make_unique(this->m_ArchiveType)); + } + + uint32_t TexturelessArchiveBuilder::setFiles() + { + uint32_t incompressibleFiles = 0; + uint32_t compressibleFiles = 0; + int count = 0; + qint64 currentSize = 0; + const auto& dirString = (this->m_RootDirectory.path() + '/').toStdWString(); + const auto& rootDirFiles = this->m_ArchiveBuilderHelper->getRootDirectoryFilenames(dirString); + qDebug() << "root is: " << m_RootDirectory.path() + '/'; + + QDirIterator iterator(this->m_RootDirectory.absolutePath(), QDir::Files, QDirIterator::Subdirectories); + while (iterator.hasNext()) { + QApplication::processEvents(); + + if (this->m_Cancelled) { + for (auto& archive : this->m_Archives) { + archive.reset(); + } + return 0; + } + + const QFileInfo& fileInfo = iterator.nextFileInfo(); + const QString& filepath = fileInfo.absoluteFilePath(); + const bool ignored = this->m_ArchiveBuilderHelper->isFileIgnorable(filepath.toStdWString(), rootDirFiles); + + Q_EMIT this->valueChanged(++count); + if (ignored || filepath.endsWith(".dds", Qt::CaseInsensitive)) { + continue; + } + + currentSize += fileInfo.size(); + if (currentSize > SIZE_LIMIT) { + currentSize = fileInfo.size(); + this->m_Archives.back()->set_compressed(!static_cast(incompressibleFiles)); + incompressibleFiles = 0; + compressibleFiles = 0; + this->m_Archives.emplace_back(std::make_unique(this->m_ArchiveType)); + this->setShareData(true); + } + + this->m_ArchiveBuilderHelper->isIncompressible(filepath.toStdWString()) ? ++incompressibleFiles : ++compressibleFiles; + auto fileBlob = disk_blob( + dirString, + filepath.toStdWString()); + this->m_Archives.back()->add_file_from_disk(fileBlob); + qDebug() << "file is: " << filepath; + } + this->m_Archives.back()->set_compressed(!static_cast(incompressibleFiles)); + return incompressibleFiles + compressibleFiles; + } + + void TexturelessArchiveBuilder::setShareData(const bool value) + { + this->m_Archives.back()->set_share_data(value); + } + + std::vector> TexturelessArchiveBuilder::getArchives() + { + return std::move(this->m_Archives); + } + + uint32_t TexturelessArchiveBuilder::getFileCount() const + { + return this->m_ArchiveBuilderHelper->getFileCount(this->m_RootDirectory.path().toStdWString()); + } + + QString TexturelessArchiveBuilder::getRootPath() const + { + return this->m_RootDirectory.path(); + } + + void TexturelessArchiveBuilder::cancel() + { + this->m_Cancelled = true; + } +} // namespace BsaPacker diff --git a/libs/bsapacker/src/bsa_packer.json b/libs/bsapacker/src/bsa_packer.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/bsapacker/src/bsa_packer.json @@ -0,0 +1 @@ +{} diff --git a/libs/bsapacker/src/bsa_packer_en.ts b/libs/bsapacker/src/bsa_packer_en.ts new file mode 100644 index 0000000..138987e --- /dev/null +++ b/libs/bsapacker/src/bsa_packer_en.ts @@ -0,0 +1,87 @@ + + + + + BsaPacker::Bsa_Packer + + + + Transform loose files into a Bethesda Softworks Archive file (.bsa/.ba2). + + + + + BSA Packer + + + + + QObject + + + Archive name (no file extension): + + + + + File " + + + + + " already exists. Overwrite? + + + + + Choose a mod to pack: + + + + + Choose a name for the packed archive: + + + + + BSA Packer + + + + + After creating the archive, set loose assets to hidden. + + + + + Create a dummy plugin to load the archive if one does not exist. + + + + + Specify a semi-colon separated list of file extensions to ignore when packing. + Specify a semi-colon seperated list of file extensions to ignore when packing. + + + + + Compress archives if they do not contain incompressible files. Texture archives for Fallout 4 and Starfield will always be compressed. Morrowind archives will never be compressed. + + + + + Writing Archive + + + + + Writing %1 + + + + + Created archive(s): + + + + diff --git a/libs/bsapacker/src/bsapacker/ArchiveAutoService.h b/libs/bsapacker/src/bsapacker/ArchiveAutoService.h new file mode 100644 index 0000000..52cc247 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/ArchiveAutoService.h @@ -0,0 +1,19 @@ +#ifndef ARCHIVEAUTOSERVICE_H +#define ARCHIVEAUTOSERVICE_H + +#include "bsapacker_global.h" +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT ArchiveAutoService : public IArchiveAutoService + { + public: + ArchiveAutoService() = default; + bool CreateBSA(libbsarch::bs_archive_auto* archive, const QString& archiveName, + bsa_archive_type_e type, const QString& sourceDir, + int nexusId) const override; + }; +} // namespace BsaPacker + +#endif // ARCHIVEAUTOSERVICE_H diff --git a/libs/bsapacker/src/bsapacker/ArchiveBuildDirector.h b/libs/bsapacker/src/bsapacker/ArchiveBuildDirector.h new file mode 100644 index 0000000..a939900 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/ArchiveBuildDirector.h @@ -0,0 +1,27 @@ +#ifndef ARCHIVEBUILDDIRECTOR_H +#define ARCHIVEBUILDDIRECTOR_H + +#include +#include + +namespace BsaPacker +{ + class ArchiveBuildDirector + { + public: + explicit ArchiveBuildDirector(const ISettingsService* settingsService, IArchiveBuilder* archiveFileBuildService); + ~ArchiveBuildDirector() = default; + ArchiveBuildDirector(const ArchiveBuildDirector&) = default; + ArchiveBuildDirector& operator=(const ArchiveBuildDirector&) = default; + ArchiveBuildDirector(ArchiveBuildDirector&&) = default; + ArchiveBuildDirector& operator=(ArchiveBuildDirector&&) = default; + + void Construct(); + + private: + IArchiveBuilder* m_ArchiveFileBuildService = nullptr; + const ISettingsService* m_SettingsService = nullptr; + }; +} // namespace BsaPacker + +#endif // ARCHIVEBUILDDIRECTOR_H diff --git a/libs/bsapacker/src/bsapacker/ArchiveBuilderFactory.h b/libs/bsapacker/src/bsapacker/ArchiveBuilderFactory.h new file mode 100644 index 0000000..98ce8e1 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/ArchiveBuilderFactory.h @@ -0,0 +1,31 @@ +#ifndef ARCHIVEBUILDERFACTORY_H +#define ARCHIVEBUILDERFACTORY_H + +#include "bsapacker_global.h" +#include +#include +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT ArchiveBuilderFactory : public IArchiveBuilderFactory + { + public: + ArchiveBuilderFactory(const IArchiveBuilderHelper* archiveBuilderHelper); + ~ArchiveBuilderFactory() override = default; + ArchiveBuilderFactory(const ArchiveBuilderFactory&) = delete; + ArchiveBuilderFactory& operator=(const ArchiveBuilderFactory&) = delete; + ArchiveBuilderFactory(ArchiveBuilderFactory&&) = delete; + ArchiveBuilderFactory& operator=(ArchiveBuilderFactory&&) = delete; + + [[nodiscard]] std::vector GetArchiveTypes(const IModDto* modDto) const override; + [[nodiscard]] std::unique_ptr Create(bsa_archive_type_e archiveType, const IModDto* modDto) const override; + + private: + const IArchiveBuilderHelper* m_ArchiveBuilderHelper = nullptr; + }; +} // namespace BsaPacker + +#endif // ARCHIVEBUILDERFACTORY_H diff --git a/libs/bsapacker/src/bsapacker/ArchiveBuilderHelper.h b/libs/bsapacker/src/bsapacker/ArchiveBuilderHelper.h new file mode 100644 index 0000000..43194d1 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/ArchiveBuilderHelper.h @@ -0,0 +1,29 @@ +#ifndef ARCHIVEBUILDERHELPER_H +#define ARCHIVEBUILDERHELPER_H + +#include "bsapacker_global.h" +#include +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT ArchiveBuilderHelper : public IArchiveBuilderHelper + { + public: + ArchiveBuilderHelper(const ISettingsService* settingsService); + [[nodiscard]] bool isFileIgnorable(const std::filesystem::path&, const std::vector&) const override; + [[nodiscard]] bool isIncompressible(const std::filesystem::path&) const override; + [[nodiscard]] bool isExtensionBlacklisted(const std::filesystem::path&) const override; + [[nodiscard]] uint32_t getFileCount(const std::filesystem::path&) const override; + [[nodiscard]] std::vector getRootDirectoryFilenames(const std::filesystem::path&) const override; + [[nodiscard]] bool doesPathContainFiles(const std::filesystem::path&, const std::vector&) const override; + + private: + const static std::set INCOMPRESSIBLE_TYPES; + const ISettingsService* m_SettingsService = nullptr; + }; +} // namespace BsaPacker + +#endif // ARCHIVEBUILDERHELPER_H diff --git a/libs/bsapacker/src/bsapacker/GeneralArchiveBuilder.h b/libs/bsapacker/src/bsapacker/GeneralArchiveBuilder.h new file mode 100644 index 0000000..3f2d387 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/GeneralArchiveBuilder.h @@ -0,0 +1,37 @@ +#ifndef GENERALARCHIVEBUILDER_H +#define GENERALARCHIVEBUILDER_H + +#include "bsapacker_global.h" +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT GeneralArchiveBuilder : public IArchiveBuilder + { + Q_OBJECT + Q_INTERFACES(BsaPacker::IEmitsValueChanged) + + public: + GeneralArchiveBuilder(const IArchiveBuilderHelper* archiveBuilderHelper, const QDir& rootDir, const bsa_archive_type_t& type); + uint32_t setFiles() override; + void setShareData(bool value) override; + [[nodiscard]] std::vector> getArchives() override; + [[nodiscard]] uint32_t getFileCount() const override; + [[nodiscard]] QString getRootPath() const override; + + public Q_SLOTS: + void cancel() override; + + private: + const IArchiveBuilderHelper* m_ArchiveBuilderHelper = nullptr; + std::vector> m_Archives; + const bsa_archive_type_t m_ArchiveType; + bool m_Cancelled; + QDir m_RootDirectory; + const static qint64 SIZE_LIMIT; + }; +} // namespace BsaPacker + +#endif // GENERALARCHIVEBUILDER_H diff --git a/libs/bsapacker/src/bsapacker/IArchiveAutoService.h b/libs/bsapacker/src/bsapacker/IArchiveAutoService.h new file mode 100644 index 0000000..03fba4a --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IArchiveAutoService.h @@ -0,0 +1,18 @@ +#ifndef IARCHIVEAUTOSERVICE_H +#define IARCHIVEAUTOSERVICE_H + +#include +#include + +namespace BsaPacker +{ + class IArchiveAutoService + { + public: + virtual ~IArchiveAutoService() = default; + virtual bool CreateBSA(libbsarch::bs_archive_auto*, const QString&, bsa_archive_type_e, + const QString& sourceDir, int nexusId) const = 0; + }; +} + +#endif // IARCHIVEAUTOSERVICE_H diff --git a/libs/bsapacker/src/bsapacker/IArchiveBuilder.h b/libs/bsapacker/src/bsapacker/IArchiveBuilder.h new file mode 100644 index 0000000..6c6ca4d --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IArchiveBuilder.h @@ -0,0 +1,24 @@ +#ifndef IARCHIVEBUILDER_H +#define IARCHIVEBUILDER_H + +#include +#include + +namespace BsaPacker +{ + class IArchiveBuilder : public IEmitsValueChanged + { + Q_OBJECT + Q_INTERFACES(BsaPacker::IEmitsValueChanged) + + public: + virtual ~IArchiveBuilder() = default; + virtual uint32_t setFiles() = 0; + virtual void setShareData(bool) = 0; + [[nodiscard]] virtual std::vector> getArchives() = 0; + [[nodiscard]] virtual uint32_t getFileCount() const = 0; + [[nodiscard]] virtual QString getRootPath() const = 0; + }; +} // namespace BsaPacker + +#endif // IARCHIVEBUILDER_H diff --git a/libs/bsapacker/src/bsapacker/IArchiveBuilderFactory.h b/libs/bsapacker/src/bsapacker/IArchiveBuilderFactory.h new file mode 100644 index 0000000..23da46d --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IArchiveBuilderFactory.h @@ -0,0 +1,18 @@ +#ifndef IARCHIVEBUILDERFACTORY_H +#define IARCHIVEBUILDERFACTORY_H + +#include +#include + +namespace BsaPacker +{ + class IArchiveBuilderFactory + { + public: + virtual ~IArchiveBuilderFactory() = default; + virtual std::vector GetArchiveTypes(const IModDto* modDto) const = 0; + virtual std::unique_ptr Create(bsa_archive_type_e archiveType, const IModDto* modDto) const = 0; + }; +} + +#endif // IARCHIVEBUILDERFACTORY_H diff --git a/libs/bsapacker/src/bsapacker/IArchiveBuilderHelper.h b/libs/bsapacker/src/bsapacker/IArchiveBuilderHelper.h new file mode 100644 index 0000000..3d697c5 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IArchiveBuilderHelper.h @@ -0,0 +1,22 @@ +#ifndef IARCHIVEBUILDERHELPER_H +#define IARCHIVEBUILDERHELPER_H + +#include +#include + +namespace BsaPacker +{ + class IArchiveBuilderHelper + { + public: + virtual ~IArchiveBuilderHelper() = default; + [[nodiscard]] virtual bool isFileIgnorable(const std::filesystem::path&, const std::vector&) const = 0; + [[nodiscard]] virtual bool isIncompressible(const std::filesystem::path&) const = 0; + [[nodiscard]] virtual bool isExtensionBlacklisted(const std::filesystem::path&) const = 0; + [[nodiscard]] virtual uint32_t getFileCount(const std::filesystem::path&) const = 0; + [[nodiscard]] virtual std::vector getRootDirectoryFilenames(const std::filesystem::path&) const = 0; + [[nodiscard]] virtual bool doesPathContainFiles(const std::filesystem::path&, const std::vector&) const = 0; + }; +} // namespace BsaPacker + +#endif // IARCHIVEBUILDERHELPER_H diff --git a/libs/bsapacker/src/bsapacker/IArchiveNameService.h b/libs/bsapacker/src/bsapacker/IArchiveNameService.h new file mode 100644 index 0000000..3853e28 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IArchiveNameService.h @@ -0,0 +1,21 @@ +#ifndef IARCHIVENAMESERVICE_H +#define IARCHIVENAMESERVICE_H + +#include +#include +#include + +namespace BsaPacker +{ + class IArchiveNameService + { + public: + virtual ~IArchiveNameService() = default; + virtual QString GetFileExtension() const = 0; + virtual QString GetArchiveFullPath(bsa_archive_type_e, const IModDto*) const = 0; + virtual QString Infix(bsa_archive_type_e type) const = 0; + virtual QString Suffix(const QString& pathNoExt) const = 0; + }; +} + +#endif // !IARCHIVENAMESERVICE_H diff --git a/libs/bsapacker/src/bsapacker/IDummyPluginLogic.h b/libs/bsapacker/src/bsapacker/IDummyPluginLogic.h new file mode 100644 index 0000000..b837d9c --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IDummyPluginLogic.h @@ -0,0 +1,19 @@ +#ifndef IDUMMYPLUGINLOGIC_H +#define IDUMMYPLUGINLOGIC_H + +#include +#include +#include + +namespace BsaPacker +{ + class IDummyPluginLogic + { + public: + virtual ~IDummyPluginLogic() = default; + [[nodiscard]] virtual bool canCreateDummyESP(const QString& fileNameNoExtension, const bsa_archive_type_e type) const = 0; + [[nodiscard]] virtual bool canCreateDummyESL(const QString& fileNameNoExtension, const bsa_archive_type_e type) const = 0; + }; +} + +#endif // IDUMMYPLUGINLOGIC_H diff --git a/libs/bsapacker/src/bsapacker/IDummyPluginService.h b/libs/bsapacker/src/bsapacker/IDummyPluginService.h new file mode 100644 index 0000000..3421337 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IDummyPluginService.h @@ -0,0 +1,17 @@ +#ifndef IDUMMYPLUGINSERVICE_H +#define IDUMMYPLUGINSERVICE_H + +#include + +namespace BsaPacker +{ + class IDummyPluginService + { + public: + virtual ~IDummyPluginService() = default; + virtual bool CreatePlugin(const QString& modPath, + const QString& archiveNameBase) const = 0; + }; +} + +#endif // IDUMMYPLUGINSERVICE_H diff --git a/libs/bsapacker/src/bsapacker/IDummyPluginServiceFactory.h b/libs/bsapacker/src/bsapacker/IDummyPluginServiceFactory.h new file mode 100644 index 0000000..157ebb1 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IDummyPluginServiceFactory.h @@ -0,0 +1,17 @@ +#ifndef IDUMMYPLUGINSERVICEFACTORY_H +#define IDUMMYPLUGINSERVICEFACTORY_H + +#include +#include + +namespace BsaPacker +{ + class IDummyPluginServiceFactory + { + public: + virtual ~IDummyPluginServiceFactory() = default; + [[nodiscard]] virtual std::unique_ptr Create() const = 0; + }; +} + +#endif // IDUMMYPLUGINSERVICEFACTORY_H diff --git a/libs/bsapacker/src/bsapacker/IEmitsValueChanged.h b/libs/bsapacker/src/bsapacker/IEmitsValueChanged.h new file mode 100644 index 0000000..b4f96d0 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IEmitsValueChanged.h @@ -0,0 +1,25 @@ +#ifndef IEMITSVALUECHANGED_H +#define IEMITSVALUECHANGED_H + +#include + +namespace BsaPacker +{ + class IEmitsValueChanged : public QObject + { + Q_OBJECT + + public: + virtual ~IEmitsValueChanged() = default; + + public Q_SLOTS: + virtual void cancel() = 0; + + Q_SIGNALS: + void valueChanged(int); + }; +} // namespace BsaPacker + +Q_DECLARE_INTERFACE(BsaPacker::IEmitsValueChanged, "IEmitsValueChanged"); + +#endif // IEMITSVALUECHANGED_H diff --git a/libs/bsapacker/src/bsapacker/IFileWriterService.h b/libs/bsapacker/src/bsapacker/IFileWriterService.h new file mode 100644 index 0000000..e855549 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IFileWriterService.h @@ -0,0 +1,16 @@ +#ifndef IFILEWRITERSERVICE_H +#define IFILEWRITERSERVICE_H + +#include + +namespace BsaPacker +{ + class IFileWriterService + { + public: + virtual ~IFileWriterService() = default; + virtual bool Write(const std::string&, const char*, uint32_t) const = 0; + }; +} + +#endif // IFILEWRITERSERVICE_H diff --git a/libs/bsapacker/src/bsapacker/IHideLooseAssetService.h b/libs/bsapacker/src/bsapacker/IHideLooseAssetService.h new file mode 100644 index 0000000..d8a92cd --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IHideLooseAssetService.h @@ -0,0 +1,16 @@ +#ifndef IHIDELOOSEASSETSERVICE_H +#define IHIDELOOSEASSETSERVICE_H + +#include + +namespace BsaPacker +{ + class IHideLooseAssetService + { + public: + virtual ~IHideLooseAssetService() = default; + virtual bool HideLooseAssets(const QDir& modDirectory) const = 0; + }; +} + +#endif // IHIDELOOSEASSETSERVICE_H diff --git a/libs/bsapacker/src/bsapacker/IModContext.h b/libs/bsapacker/src/bsapacker/IModContext.h new file mode 100644 index 0000000..197854c --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IModContext.h @@ -0,0 +1,20 @@ +#ifndef IMODCONTEXT_H +#define IMODCONTEXT_H + +#include +#include + +namespace BsaPacker +{ + class IModContext + { + public: + virtual ~IModContext() = default; + [[nodiscard]] virtual QString GetAbsoluteModPath(const QString& modName) const = 0; + [[nodiscard]] virtual int GetNexusId() const = 0; + [[nodiscard]] virtual QStringList GetPlugins(const QDir& modDirectory) const = 0; + [[nodiscard]] virtual QStringList GetValidMods() const = 0; + }; +} // namespace BsaPacker + +#endif // IMODCONTEXT_H diff --git a/libs/bsapacker/src/bsapacker/IModDto.h b/libs/bsapacker/src/bsapacker/IModDto.h new file mode 100644 index 0000000..621fc57 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IModDto.h @@ -0,0 +1,20 @@ +#ifndef IMODDTO_H +#define IMODDTO_H + +#include + +namespace BsaPacker +{ + class IModDto + { + public: + virtual ~IModDto() = default; + [[nodiscard]] virtual QString ArchiveExtension() const = 0; + [[nodiscard]] virtual QString ArchiveName() const = 0; + [[nodiscard]] virtual QString Directory() const = 0; + [[nodiscard]] virtual QString ModForename() const = 0; + [[nodiscard]] virtual int NexusId() const = 0; + }; +} // namespace BsaPacker + +#endif // IMODDTO_H diff --git a/libs/bsapacker/src/bsapacker/IModDtoFactory.h b/libs/bsapacker/src/bsapacker/IModDtoFactory.h new file mode 100644 index 0000000..ea97bbe --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IModDtoFactory.h @@ -0,0 +1,17 @@ +#ifndef IMODDTOFACTORY_H +#define IMODDTOFACTORY_H + +#include "IModDto.h" +#include + +namespace BsaPacker +{ + class IModDtoFactory + { + public: + virtual ~IModDtoFactory() = default; + [[nodiscard]] virtual std::unique_ptr Create() const = 0; + }; +} // namespace BsaPacker + +#endif // IMODDTOFACTORY_H diff --git a/libs/bsapacker/src/bsapacker/IOverrideFileService.h b/libs/bsapacker/src/bsapacker/IOverrideFileService.h new file mode 100644 index 0000000..6e87c48 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IOverrideFileService.h @@ -0,0 +1,16 @@ +#ifndef IOVERRIDEFILESERVICE_H +#define IOVERRIDEFILESERVICE_H + +#include + +namespace BsaPacker { + class IOverrideFileService { + public: + virtual ~IOverrideFileService() = default; + virtual bool CreateOverrideFile(const int nexusId, + const QString& modPath, + const QStringList& archiveNames) const = 0; + }; +} // namespace BsaPacker + +#endif // IOVERRIDEFILESERVICE_H diff --git a/libs/bsapacker/src/bsapacker/IPackerDialog.h b/libs/bsapacker/src/bsapacker/IPackerDialog.h new file mode 100644 index 0000000..b4fc38c --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IPackerDialog.h @@ -0,0 +1,25 @@ +#ifndef IPACKERDIALOG_H +#define IPACKERDIALOG_H + +#include +#include +#include + +namespace BsaPacker +{ + class IPackerDialog + { + public: + virtual ~IPackerDialog() = default; + [[nodiscard]] virtual bool IsNewFilename() const = 0; + [[nodiscard]] virtual QString SelectedMod() const = 0; + virtual void RefreshModList() = 0; + [[nodiscard]] virtual QString SelectedPluginItem() const = 0; + virtual void UpdateNameList(const QString& modName) = 0; + virtual void RefreshSelectedName() = 0; + virtual int Exec() = 0; + virtual void RefreshOkButton() = 0; + }; +} // namespace BsaPacker + +#endif // IPACKERDIALOG_H diff --git a/libs/bsapacker/src/bsapacker/IPackerDialogFactory.h b/libs/bsapacker/src/bsapacker/IPackerDialogFactory.h new file mode 100644 index 0000000..1123ccc --- /dev/null +++ b/libs/bsapacker/src/bsapacker/IPackerDialogFactory.h @@ -0,0 +1,16 @@ +#ifndef IPACKERDIALOGFACTORY_H +#define IPACKERDIALOGFACTORY_H + +#include + +namespace BsaPacker +{ + class IPackerDialogFactory + { + public: + virtual ~IPackerDialogFactory() = default; + virtual std::unique_ptr Create() const = 0; + }; +} + +#endif // IPACKERDIALOGFACTORY_H diff --git a/libs/bsapacker/src/bsapacker/ISettingsService.h b/libs/bsapacker/src/bsapacker/ISettingsService.h new file mode 100644 index 0000000..75a03c9 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/ISettingsService.h @@ -0,0 +1,17 @@ +#ifndef ISETTINGSSERVICE_H +#define ISETTINGSSERVICE_H + +#include +#include + +namespace BsaPacker +{ + class ISettingsService + { + public: + virtual ~ISettingsService() = default; + [[nodiscard]] virtual QVariant GetPluginSetting(const QString&) const = 0; + }; +} + +#endif // ISETTINGSSERVICE_H diff --git a/libs/bsapacker/src/bsapacker/ModDtoFactory.h b/libs/bsapacker/src/bsapacker/ModDtoFactory.h new file mode 100644 index 0000000..eb28372 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/ModDtoFactory.h @@ -0,0 +1,33 @@ +#ifndef MODDTOFACTORY_H +#define MODDTOFACTORY_H + +#include "bsapacker_global.h" +#include +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT ModDtoFactory : public IModDtoFactory + { + public: + ModDtoFactory( + const IModContext* modContext, + IPackerDialog* packerDialog); + ~ModDtoFactory() override = default; + [[nodiscard]] std::unique_ptr Create() const override; + [[nodiscard]] static QString ArchiveNameValidator( + const QString& modName, + const QString& pluginName, + const bool needsNewName); + [[nodiscard]] static bool CanOverwriteFile( + const QString& filePath, + const QString& fileName); + + private: + const IModContext* m_ModContext = nullptr; + IPackerDialog* m_PackerDialog = nullptr; + }; +} // namespace BsaPacker +#endif // MODDTOFACTORY_H diff --git a/libs/bsapacker/src/bsapacker/NullArchiveBuilder.h b/libs/bsapacker/src/bsapacker/NullArchiveBuilder.h new file mode 100644 index 0000000..c69cea7 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/NullArchiveBuilder.h @@ -0,0 +1,28 @@ +#ifndef NULLARCHIVEBUILDER_H +#define NULLARCHIVEBUILDER_H + +#include + +namespace BsaPacker +{ + class NullArchiveBuilder : public IArchiveBuilder + { + Q_OBJECT + Q_INTERFACES(BsaPacker::IEmitsValueChanged) + + public: + NullArchiveBuilder() = default; + ~NullArchiveBuilder() override = default; + + uint32_t setFiles() override; + void setShareData(bool) override; + [[nodiscard]] std::vector> getArchives() override; + [[nodiscard]] uint32_t getFileCount() const override; + [[nodiscard]] QString getRootPath() const override; + + public Q_SLOTS: + void cancel() override; + }; +} // namespace BsaPacker + +#endif // NULLARCHIVEBUILDER_H diff --git a/libs/bsapacker/src/bsapacker/TextureArchiveBuilder.h b/libs/bsapacker/src/bsapacker/TextureArchiveBuilder.h new file mode 100644 index 0000000..c785931 --- /dev/null +++ b/libs/bsapacker/src/bsapacker/TextureArchiveBuilder.h @@ -0,0 +1,41 @@ +#ifndef TEXTUREARCHIVEBUILDER_H +#define TEXTUREARCHIVEBUILDER_H + +#include "bsapacker_global.h" +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT TextureArchiveBuilder : public IArchiveBuilder + { + Q_OBJECT + Q_INTERFACES(BsaPacker::IEmitsValueChanged) + + public: + TextureArchiveBuilder(const IArchiveBuilderHelper* archiveBuilderHelper, const QDir&, const bsa_archive_type_t& type); + ~TextureArchiveBuilder() override = default; + + uint32_t setFiles() override; + void setShareData(bool) override; + [[nodiscard]] std::vector> getArchives() override; + [[nodiscard]] uint32_t getFileCount() const override; + [[nodiscard]] QString getRootPath() const override; + + public Q_SLOTS: + void cancel() override; + + private: + const IArchiveBuilderHelper* m_ArchiveBuilderHelper = nullptr; + std::vector> m_Archives; + const bsa_archive_type_t m_ArchiveType; + bool m_Cancelled; + QDir m_RootDirectory; + const static qint64 SIZE_LIMIT; + + static void DDSCallback(bsa_archive_t archive, const wchar_t* file_path, bsa_dds_info_t* dds_info, void* context); + }; +} // namespace BsaPacker + +#endif // TEXTUREARCHIVEBUILDER_H diff --git a/libs/bsapacker/src/bsapacker/TexturelessArchiveBuilder.h b/libs/bsapacker/src/bsapacker/TexturelessArchiveBuilder.h new file mode 100644 index 0000000..7c1b6af --- /dev/null +++ b/libs/bsapacker/src/bsapacker/TexturelessArchiveBuilder.h @@ -0,0 +1,39 @@ +#ifndef TEXTURELESSARCHIVEBUILDER_H +#define TEXTURELESSARCHIVEBUILDER_H + +#include "bsapacker_global.h" +#include +#include +#include + +namespace BsaPacker +{ + class BSAPACKER_EXPORT TexturelessArchiveBuilder : public IArchiveBuilder + { + Q_OBJECT + Q_INTERFACES(BsaPacker::IEmitsValueChanged) + + public: + TexturelessArchiveBuilder(const IArchiveBuilderHelper* archiveBuilderHelper, const QDir& rootDir, const bsa_archive_type_t& type); + ~TexturelessArchiveBuilder() override = default; + + uint32_t setFiles() override; + void setShareData(bool) override; + [[nodiscard]] std::vector> getArchives() override; + [[nodiscard]] uint32_t getFileCount() const override; + [[nodiscard]] QString getRootPath() const override; + + public Q_SLOTS: + void cancel() override; + + private: + const IArchiveBuilderHelper* m_ArchiveBuilderHelper = nullptr; + std::vector> m_Archives; + const bsa_archive_type_t m_ArchiveType; + bool m_Cancelled; + QDir m_RootDirectory; + const static qint64 SIZE_LIMIT; + }; +} // namespace BsaPacker + +#endif // TEXTURELESSARCHIVEBUILDER_H diff --git a/libs/bsapacker/src/bsapacker_global.h b/libs/bsapacker/src/bsapacker_global.h new file mode 100644 index 0000000..2ad3dc3 --- /dev/null +++ b/libs/bsapacker/src/bsapacker_global.h @@ -0,0 +1,7 @@ +#include + +#if defined(BSAPACKER_LIBRARY) +# define BSAPACKER_EXPORT Q_DECL_EXPORT +#else +# define BSAPACKER_EXPORT Q_DECL_IMPORT +#endif diff --git a/libs/bsapacker/src/qlibbsarch/BSArchive.h b/libs/bsapacker/src/qlibbsarch/BSArchive.h new file mode 100644 index 0000000..a159ffd --- /dev/null +++ b/libs/bsapacker/src/qlibbsarch/BSArchive.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include "BSArchiveEntries.h" +#include + +class BSArchive +{ +public: + BSArchive(); + virtual ~BSArchive(); + + void open(const QString& archivePath); + void close(); + void create(const QString &archiveName, const bsa_archive_type_e &type, const BSArchiveEntries &entries); + void save(); + void addFileFromDiskRoot(const QString &rootDir, const QString &filename); + void addFileFromDiskRoot(const QString &rootDir, const QStringList &files); + void addFileFromDisk(const QString& pathInArchive, const QString& filePath); + void addFileFromMemory(const QString& filename, const QByteArray& data); + void setCompressed(bool value); + void setShareData(bool value); + bsa_file_record_t findFileRecord (const QString& filename); + QByteArray extractFileDataByRecord(bsa_file_record_t record); + QByteArray extractFileDataByFilename(const QString& filename); + void extract(const QString& filename, const QString& saveAs); + QStringList listFiles(); + void free(); + void reset(); + + void setDDSCallback(bsa_file_dds_info_proc_t fileDDSInfoProc, void *context); + + bsa_archive_t getArchive() const; + +private: + bool _openedArchive = false; + bsa_archive_t _archive; +}; diff --git a/libs/bsapacker/src/qlibbsarch/BSArchiveAuto.h b/libs/bsapacker/src/qlibbsarch/BSArchiveAuto.h new file mode 100644 index 0000000..d632fa4 --- /dev/null +++ b/libs/bsapacker/src/qlibbsarch/BSArchiveAuto.h @@ -0,0 +1,91 @@ +#pragma once + +#include "BSArchive.h" +#include "BSArchiveEntries.h" +#include + +/*! + * \brief A convenience class for BSArchive and BSArchiveEntries. Its performance is worse than using these + * two classes separately, but it removes the need to manually handle the BSArchiveEntries. + */ +class BSArchiveAuto +{ +public: + /*! + * \brief Constructor + * \param rootDirectory The root directory of the BSA. This directory is the one containing folders such as textures and meshes. + */ + BSArchiveAuto(const QString &_rootDirectory); + /*! + * \brief Opens an existing archive + * \param archivePath The path of the archive + */ + void open(const QString &archivePath); + /*! + * \brief Creates a BSA in memory + * \param archiveName The BSA name + * \param type The BSA type + */ + void create(const QString& archiveName, const bsa_archive_type_e& type); + /*! + * \brief Adds a file from the disk to the BSA. Also adds it to the BSA entries. + * \param rootDir The root directory of the file. + * \param filename The complete path to the file. + */ + void addFileFromDiskRoot(const QString& filename); + /*! + * \brief Adds files from the disk to the BSA. Also adds them to the BSA entries. + * \param rootDir The root directory of the file. This directory is the one containing folders such as textures and meshes. + * \param files A list containing the complete paths to the files. + */ + void addFileFromDiskRoot(const QStringList& files); + /*! + * \brief Adds file from the memory to the BSA. Also adds it to the BSA entries. + * \param filename The name of the file. It connot be a full path, and has to be a relative path. + * \param data The file bytes data. + */ + void addFileFromMemory(const QString& filename, const QByteArray& data); + /*! + * \brief Adds a file from the disk to the BSA. Also adds it to the BSA entries. + * \param saveAs The path of the file in the BSA + * \param diskPath The path of the file on disk + */ + void addFileFromDisk(const QString& saveAs, const QString& diskPath); + /*! + * \brief Adds files from the disk to the BSA. Also adds it to the BSA entries. + * \param map A map containing "save as" path as keys and disk path as values + */ + void addFileFromDisk(const QMap& map); + /*! + * \brief Extracts all files from the BSA to the destination. + * \param destinationDirectory The directory where all files will be extracted. + * \param overwriteExistingFiles Whether files in archive will overwrite existing loose files or not + */ + void extractAll(const QString &destinationDirectory, const bool &overwriteExistingFiles); + /*! + * \brief Saves the archive to the disk + */ + void save(); + + void setShareData(const bool state); + void setCompressed(const bool state); + + void reset(); + /*! + * \brief Required in order to create FO4 DDS archive + * \param fileDDSInfoProc The function that will process the DDS file + */ + void setDDSCallback(bsa_file_dds_info_proc_t fileDDSInfoProc, void *context); + +private: + /*! + * \brief The key will store the "save as" path, while the value will hold the disk path + */ + QMap _filesFromDisk; + QStringList _filesFromDiskRoot; + QMap _filesfromMemory; + QDir _rootDirectory; + + BSArchive _archive; + BSArchiveEntries _entries; +}; diff --git a/libs/bsapacker/src/qlibbsarch/BSArchiveEntries.h b/libs/bsapacker/src/qlibbsarch/BSArchiveEntries.h new file mode 100644 index 0000000..3f3cb63 --- /dev/null +++ b/libs/bsapacker/src/qlibbsarch/BSArchiveEntries.h @@ -0,0 +1,23 @@ +#pragma once + +#include "QLibbsarch.h" + +class BSArchiveEntries +{ +public: + BSArchiveEntries(); + explicit BSArchiveEntries(const QStringList& QSLEntries); + explicit BSArchiveEntries(const bsa_entry_list_t& entries); + ~BSArchiveEntries(); + + void add(const QString& filepath); + void free(); + void reset(); + uint32_t count() ; + QStringList list(); + + bsa_entry_list_t getEntries() const; + +private: + bsa_entry_list_t _entries; +}; diff --git a/libs/bsapacker/src/qlibbsarch/QLibbsarch.h b/libs/bsapacker/src/qlibbsarch/QLibbsarch.h new file mode 100644 index 0000000..7af53f4 --- /dev/null +++ b/libs/bsapacker/src/qlibbsarch/QLibbsarch.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace QLibBsarch +{ + constexpr bool enableDebugLog = true; + +#define LOG_LIBBSARCH \ + if constexpr (QLibBsarch::enableDebugLog) \ + qDebug() << "[QLIBBSARCH] " << __FUNCTION__ << ' ' + +#define PREPARE_PATH_LIBBSARCH(qstring) reinterpret_cast(QDir::toNativeSeparators(qstring).utf16()) + + inline const std::string wcharToString(const wchar_t *text) { return QString::fromWCharArray(text).toStdString(); } + + inline void checkResult(const bsa_result_message_s &result) + { + if (result.code == BSA_RESULT_EXCEPTION) + { + const std::string &error = QLibBsarch::wcharToString(result.text); + LOG_LIBBSARCH << QString::fromStdString(error); + throw std::runtime_error(error); + } + } + + inline void checkResult(const bsa_result_message_buffer_s &result) + { + if (result.message.code == BSA_RESULT_EXCEPTION) + { + const std::string &error = QLibBsarch::wcharToString(result.message.text); + LOG_LIBBSARCH << QString::fromStdString(error); + throw std::runtime_error(error); + } + } + +} // namespace QLibBsarch diff --git a/libs/bsapacker/tests/ArchiveAutoServiceFacts.cpp b/libs/bsapacker/tests/ArchiveAutoServiceFacts.cpp new file mode 100644 index 0000000..8fd37f1 --- /dev/null +++ b/libs/bsapacker/tests/ArchiveAutoServiceFacts.cpp @@ -0,0 +1,26 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include + +using namespace BsaPacker; + +namespace BsaPackerTests +{ + class ArchiveAutoServiceFacts : public ::testing::Test + { + protected: + void SetUp() override { + } + + void TearDown() override { + } + }; + + TEST_F(ArchiveAutoServiceFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = ArchiveAutoService(); + ); + } +} diff --git a/libs/bsapacker/tests/ArchiveBuilderFactoryFacts.cpp b/libs/bsapacker/tests/ArchiveBuilderFactoryFacts.cpp new file mode 100644 index 0000000..352fc00 --- /dev/null +++ b/libs/bsapacker/tests/ArchiveBuilderFactoryFacts.cpp @@ -0,0 +1,242 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include +#include "MockArchiveBuilderHelper.h" +#include "ModDto.h" +#include "NexusId.h" +#include +#include +#include +#include +#include + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class ArchiveBuilderFactoryFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockArchiveBuilderHelper = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockArchiveBuilderHelper; + } + + NaggyMock* naggyMockArchiveBuilderHelper; + }; + + TEST_F(ArchiveBuilderFactoryFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + ); + } + + TEST_F(ArchiveBuilderFactoryFacts, GetArchiveTypes_WhenNexusIdIsMorrowind_ReturnsbaTES3) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto dto = ModDto(NexusId::Morrowind, "", "", ""); + + // Act + auto result = sut.GetArchiveTypes(&dto); + + // Assert + EXPECT_EQ(bsa_archive_type_e::baTES3, result.at(0)); + } + + TEST_F(ArchiveBuilderFactoryFacts, GetArchiveTypes_WhenNexusIdIsOblivion_ReturnsbaTES4) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto dto = ModDto(NexusId::Oblivion, "", "", ""); + + // Act + auto result = sut.GetArchiveTypes(&dto); + + // Assert + EXPECT_EQ(bsa_archive_type_e::baTES4, result.at(0)); + } + + TEST_F(ArchiveBuilderFactoryFacts, GetArchiveTypes_WhenNexusIdIsFallout3_ReturnsbaFO3) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto dto = ModDto(NexusId::Fallout3, "", "", ""); + + // Act + auto result = sut.GetArchiveTypes(&dto); + + // Assert + EXPECT_EQ(bsa_archive_type_e::baFO3, result.at(0)); + } + + TEST_F(ArchiveBuilderFactoryFacts, GetArchiveTypes_WhenNexusIdIsNewVegas_ReturnsbaFO3) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto dto = ModDto(NexusId::NewVegas, "", "", ""); + + // Act + auto result = sut.GetArchiveTypes(&dto); + + // Assert + EXPECT_EQ(bsa_archive_type_e::baFO3, result.at(0)); + } + + TEST_F(ArchiveBuilderFactoryFacts, GetArchiveTypes_WhenNexusIdIsSkyrim_ReturnsbaFO3) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto dto = ModDto(NexusId::Skyrim, "", "", ""); + + // Act + auto result = sut.GetArchiveTypes(&dto); + + // Assert + EXPECT_EQ(bsa_archive_type_e::baFO3, result.at(0)); + } + + TEST_F(ArchiveBuilderFactoryFacts, GetArchiveTypes_WhenNexusIdIsSkyrimSE_ReturnsbaSSE) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto dto = ModDto(NexusId::SkyrimSE, "", "", ""); + + // Act + auto result = sut.GetArchiveTypes(&dto); + + // Assert + EXPECT_EQ(bsa_archive_type_e::baSSE, result.at(0)); + } + + TEST_F(ArchiveBuilderFactoryFacts, GetArchiveTypes_WhenNexusIdIsFallout4_ReturnsbaFO4andbaFO4dds) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto dto = ModDto(NexusId::Fallout4, "", "", ""); + + // Act + auto result = sut.GetArchiveTypes(&dto); + + // Assert + EXPECT_EQ(bsa_archive_type_e::baFO4, result.at(0)); + EXPECT_EQ(bsa_archive_type_e::baFO4dds, result.at(1)); + } + + TEST_F(ArchiveBuilderFactoryFacts, Create_WhenArchiveTypeNone_ReturnsNullArchiveBuilder) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto type = bsa_archive_type_e::baNone; + + // Act + auto returned = sut.Create(type, nullptr); + auto result = returned.get(); + + // Assert + auto resultType = dynamic_cast(result); + EXPECT_STREQ(typeid(NullArchiveBuilder).name(), typeid(*resultType).name()); + } + + TEST_F(ArchiveBuilderFactoryFacts, Create_WhenArchiveTypeTES3_ReturnsGeneralArchiveBuilder) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto type = bsa_archive_type_e::baTES3; + auto dto = ModDto(0, "", "", ""); + + // Act + auto returned = sut.Create(type, &dto); + auto result = returned.get(); + + // Assert + auto resultType = dynamic_cast(result); + EXPECT_STREQ(typeid(GeneralArchiveBuilder).name(), typeid(*resultType).name()); + } + + TEST_F(ArchiveBuilderFactoryFacts, Create_WhenArchiveTypeTES4_ReturnsGeneralArchiveBuilder) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto type = bsa_archive_type_e::baTES4; + auto dto = ModDto(0, "", "", ""); + + // Act + auto returned = sut.Create(type, &dto); + auto result = returned.get(); + + // Assert + auto resultType = dynamic_cast(result); + EXPECT_STREQ(typeid(GeneralArchiveBuilder).name(), typeid(*resultType).name()); + } + + TEST_F(ArchiveBuilderFactoryFacts, Create_WhenArchiveTypeFO3_ReturnsGeneralArchiveBuilder) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto type = bsa_archive_type_e::baFO3; + auto dto = ModDto(0, "", "", ""); + + // Act + auto returned = sut.Create(type, &dto); + auto result = returned.get(); + + // Assert + auto resultType = dynamic_cast(result); + EXPECT_STREQ(typeid(GeneralArchiveBuilder).name(), typeid(*resultType).name()); + } + + TEST_F(ArchiveBuilderFactoryFacts, Create_WhenArchiveTypeSSE_ReturnsGeneralArchiveBuilder) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto type = bsa_archive_type_e::baSSE; + auto dto = ModDto(0, "", "", ""); + + // Act + auto returned = sut.Create(type, &dto); + auto result = returned.get(); + + // Assert + auto resultType = dynamic_cast(result); + EXPECT_STREQ(typeid(GeneralArchiveBuilder).name(), typeid(*resultType).name()); + } + + TEST_F(ArchiveBuilderFactoryFacts, Create_WhenArchiveTypeFO4_ReturnsTexturelessArchiveBuilder) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto type = bsa_archive_type_e::baFO4; + auto dto = ModDto(0, "", "", ""); + + // Act + auto returned = sut.Create(type, &dto); + auto result = returned.get(); + + // Assert + auto resultType = dynamic_cast(result); + EXPECT_STREQ(typeid(TexturelessArchiveBuilder).name(), typeid(*resultType).name()); + } + + TEST_F(ArchiveBuilderFactoryFacts, Create_WhenArchiveTypeFO4dds_ReturnsTextureArchiveBuilder) + { + // Arrange + auto sut = ArchiveBuilderFactory(naggyMockArchiveBuilderHelper); + auto type = bsa_archive_type_e::baFO4dds; + auto dto = ModDto(0, "", "", ""); + + // Act + auto returned = sut.Create(type, &dto); + auto result = returned.get(); + + // Assert + auto resultType = dynamic_cast(result); + EXPECT_STREQ(typeid(TextureArchiveBuilder).name(), typeid(*resultType).name()); + } +} diff --git a/libs/bsapacker/tests/ArchiveBuilderHelperFacts.cpp b/libs/bsapacker/tests/ArchiveBuilderHelperFacts.cpp new file mode 100644 index 0000000..a970c24 --- /dev/null +++ b/libs/bsapacker/tests/ArchiveBuilderHelperFacts.cpp @@ -0,0 +1,37 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include +#include "MockSettingsService.h" + +using namespace BsaPacker; +using std::filesystem::path; +using ::testing::NaggyMock; +using ::testing::Return; + +namespace BsaPackerTests +{ + class ArchiveBuilderHelperFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockSettingsService = new NaggyMock(); + naggyPath = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockSettingsService; + delete naggyPath; + } + + NaggyMock* naggyMockSettingsService; + NaggyMock* naggyPath; + }; + + TEST_F(ArchiveBuilderHelperFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = ArchiveBuilderHelper(naggyMockSettingsService); + ); + } +} diff --git a/libs/bsapacker/tests/ArchiveNameServiceFacts.cpp b/libs/bsapacker/tests/ArchiveNameServiceFacts.cpp new file mode 100644 index 0000000..c79c545 --- /dev/null +++ b/libs/bsapacker/tests/ArchiveNameServiceFacts.cpp @@ -0,0 +1,260 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "ArchiveNameService.h" +#include "MockModContext.h" +#include "ModDto.h" +#include "NexusId.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; +using ::testing::Eq; +using ::testing::Return; + +namespace BsaPackerTests +{ + class ArchiveNameServiceFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockModContext = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockModContext; + } + + NaggyMock* naggyMockModContext; + }; + + TEST_F(ArchiveNameServiceFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = ArchiveNameService( + naggyMockModContext + ); + ); + } + + TEST_F(ArchiveNameServiceFacts, GetFileExtension_WhenNexusIdIsMorrowind_Returnsbsa) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::Morrowind)); + + // Act + auto result = sut.GetFileExtension(); + + // Assert + EXPECT_EQ(".bsa", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetFileExtension_WhenNexusIdIsOblivion_Returnsbsa) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::Oblivion)); + + // Act + auto result = sut.GetFileExtension(); + + // Assert + EXPECT_EQ(".bsa", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetFileExtension_WhenNexusIdIsFallout3_Returnsbsa) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::Fallout3)); + + // Act + auto result = sut.GetFileExtension(); + + // Assert + EXPECT_EQ(".bsa", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetFileExtension_WhenNexusIdIsNewVegas_Returnsbsa) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::NewVegas)); + + // Act + auto result = sut.GetFileExtension(); + + // Assert + EXPECT_EQ(".bsa", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetFileExtension_WhenNexusIdIsSkyrim_Returnsbsa) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::Skyrim)); + + // Act + auto result = sut.GetFileExtension(); + + // Assert + EXPECT_EQ(".bsa", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetFileExtension_WhenNexusIdIsSkyrimSE_Returnsbsa) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::SkyrimSE)); + + // Act + auto result = sut.GetFileExtension(); + + // Assert + EXPECT_EQ(".bsa", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetFileExtension_WhenNexusIdIsFallout4_Returnsbsa) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::Fallout4)); + + // Act + auto result = sut.GetFileExtension(); + + // Assert + EXPECT_EQ(".ba2", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetFileExtension_InvalidNexusId_Returnsbsa) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(0)); + + // Act + auto result = sut.GetFileExtension(); + + // Assert + EXPECT_EQ("", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetArchiveFullPath_WhenTypeIsbaFO4_ReturnsFullPathWithMainInfix) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + auto type = bsa_archive_type_e::baFO4; + auto dto = ModDto(0, "C:/directory", "archive", ".ba2"); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::Fallout4)); + + // Act + auto result = sut.GetArchiveFullPath(type, &dto); + + // Assert + EXPECT_EQ("C:/directory/archive - Main.ba2", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetArchiveFullPath_WhenTypeIsbaFO4dds_ReturnsFullPathWithTextureInfix) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + auto type = bsa_archive_type_e::baFO4dds; + auto dto = ModDto(0, "C:/directory", "archive", ".ba2"); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::Fallout4)); + + // Act + auto result = sut.GetArchiveFullPath(type, &dto); + + // Assert + EXPECT_EQ("C:/directory/archive - Textures.ba2", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetArchiveFullPath_WhenTypeIsbaTES3_ReturnsFullPathWithoutInfix) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + auto type = bsa_archive_type_e::baTES3; + auto dto = ModDto(0, "C:/directory", "archive", ".ba2"); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::Morrowind)); + + // Act + auto result = sut.GetArchiveFullPath(type, &dto); + + // Assert + EXPECT_EQ("C:/directory/archive.bsa", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetArchiveFullPath_WhenTypeIsbaTES4_ReturnsFullPathWithoutInfix) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + auto type = bsa_archive_type_e::baTES3; + auto dto = ModDto(0, "C:/directory", "archive", ".ba2"); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::Oblivion)); + + // Act + auto result = sut.GetArchiveFullPath(type, &dto); + + // Assert + EXPECT_EQ("C:/directory/archive.bsa", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetArchiveFullPath_WhenTypeIsbaFO3_ReturnsFullPathWithoutInfix) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + auto type = bsa_archive_type_e::baFO3; + auto dto = ModDto(0, "C:/directory", "archive", ".ba2"); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::Fallout3)); + + // Act + auto result = sut.GetArchiveFullPath(type, &dto); + + // Assert + EXPECT_EQ("C:/directory/archive.bsa", result.toStdString()); + } + + TEST_F(ArchiveNameServiceFacts, GetArchiveFullPath_WhenTypeIsbaSSE_ReturnsFullPathWithoutInfix) + { + // Arrange + auto sut = ArchiveNameService( + naggyMockModContext); + auto type = bsa_archive_type_e::baSSE; + auto dto = ModDto(0, "C:/directory", "archive", ".ba2"); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(NexusId::SkyrimSE)); + + // Act + auto result = sut.GetArchiveFullPath(type, &dto); + + // Assert + EXPECT_EQ("C:/directory/archive.bsa", result.toStdString()); + } +} diff --git a/libs/bsapacker/tests/BsaPackerFacts.cpp b/libs/bsapacker/tests/BsaPackerFacts.cpp new file mode 100644 index 0000000..d3e8e59 --- /dev/null +++ b/libs/bsapacker/tests/BsaPackerFacts.cpp @@ -0,0 +1,34 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "BsaPacker.h" +#include "MockOrganizer.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; +using ::testing::Return; +using ::testing::Eq; + +namespace BsaPackerTests +{ + class BsaPackerFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockOrganizer = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockOrganizer; + } + + NaggyMock* naggyMockOrganizer; + }; + + TEST_F(BsaPackerFacts, CtorAndInit_Always_Constructs) + { + auto result = Bsa_Packer(); + EXPECT_NO_THROW(result.init(naggyMockOrganizer)); + } + +} diff --git a/libs/bsapacker/tests/BsaPackerWorkerFacts.cpp b/libs/bsapacker/tests/BsaPackerWorkerFacts.cpp new file mode 100644 index 0000000..851ae3c --- /dev/null +++ b/libs/bsapacker/tests/BsaPackerWorkerFacts.cpp @@ -0,0 +1,67 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "BsaPackerWorker.h" +#include "MockSettingsService.h" +#include "MockModDtoFactory.h" +#include "MockArchiveBuilderFactory.h" +#include "MockDummyPluginServiceFactory.h" +#include "MockArchiveAutoService.h" +#include "MockHideLooseAssetService.h" +#include "MockArchiveNameService.h" +#include "MockOverrideFileService.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class BsaPackerWorkerFacts : public ::testing::Test + { + protected: + void SetUp() override + { + naggyMockSettingsService = new NaggyMock(); + naggyMockModDtoFactory = new NaggyMock(); + naggyMockArchiveBuilderFactory = new NaggyMock(); + naggyMockArchiveAutoService = new NaggyMock(); + naggyMockDummyPluginServiceFactory = new NaggyMock(); + naggyMockHideLooseAssetService = new NaggyMock(); + naggyMockArchiveNameService = new NaggyMock(); + naggyOverrideFileService = new NaggyMock(); + } + + void TearDown() override + { + delete naggyMockSettingsService; + delete naggyMockModDtoFactory; + delete naggyMockArchiveBuilderFactory; + delete naggyMockArchiveAutoService; + delete naggyMockDummyPluginServiceFactory; + delete naggyMockHideLooseAssetService; + delete naggyMockArchiveNameService; + } + + NaggyMock *naggyMockSettingsService; + NaggyMock *naggyMockModDtoFactory; + NaggyMock *naggyMockArchiveBuilderFactory; + NaggyMock *naggyMockArchiveAutoService; + NaggyMock *naggyMockDummyPluginServiceFactory; + NaggyMock *naggyMockHideLooseAssetService; + NaggyMock *naggyMockArchiveNameService; + NaggyMock *naggyOverrideFileService; + }; + + TEST_F(BsaPackerWorkerFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW(BsaPackerWorker( + naggyMockSettingsService, + naggyMockModDtoFactory, + naggyMockArchiveBuilderFactory, + naggyMockArchiveAutoService, + naggyMockDummyPluginServiceFactory, + naggyMockHideLooseAssetService, + naggyMockArchiveNameService, + naggyOverrideFileService)); + } +} diff --git a/libs/bsapacker/tests/CMakeLists.txt b/libs/bsapacker/tests/CMakeLists.txt new file mode 100644 index 0000000..5f482a7 --- /dev/null +++ b/libs/bsapacker/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.22) + +add_executable(bsa_packer_tests) +mo2_configure_tests(bsa_packer_tests WARNINGS OFF) +target_link_libraries(bsa_packer_tests PRIVATE bsa_packer) diff --git a/libs/bsapacker/tests/DummyPluginServiceFactoryFacts.cpp b/libs/bsapacker/tests/DummyPluginServiceFactoryFacts.cpp new file mode 100644 index 0000000..c662438 --- /dev/null +++ b/libs/bsapacker/tests/DummyPluginServiceFactoryFacts.cpp @@ -0,0 +1,44 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "DummyPluginServiceFactory.h" +#include "MockModContext.h" +#include "MockFileWriterService.h" +#include "MockDummyPluginLogic.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class DummyPluginServiceFactoryFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockModContext = new NaggyMock(); + naggyMockFileWriterService = new NaggyMock(); + naggyMockDummyPluginLogic = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockModContext; + delete naggyMockFileWriterService; + delete naggyMockDummyPluginLogic; + } + + NaggyMock* naggyMockModContext; + NaggyMock* naggyMockFileWriterService; + NaggyMock* naggyMockDummyPluginLogic; + }; + + TEST_F(DummyPluginServiceFactoryFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = DummyPluginServiceFactory( + naggyMockModContext, + naggyMockFileWriterService, + naggyMockDummyPluginLogic + ); + ); + } +} diff --git a/libs/bsapacker/tests/Fallout3DummyPluginServiceFacts.cpp b/libs/bsapacker/tests/Fallout3DummyPluginServiceFacts.cpp new file mode 100644 index 0000000..e485d56 --- /dev/null +++ b/libs/bsapacker/tests/Fallout3DummyPluginServiceFacts.cpp @@ -0,0 +1,39 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "Fallout3DummyPluginService.h" +#include "MockFileWriterService.h" +#include "MockDummyPluginLogic.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class Fallout3DummyPluginServiceFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockFileWriterService = new NaggyMock(); + naggyMockDummyPluginLogic = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockFileWriterService; + delete naggyMockDummyPluginLogic; + } + + NaggyMock* naggyMockFileWriterService; + NaggyMock* naggyMockDummyPluginLogic; + }; + + TEST_F(Fallout3DummyPluginServiceFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = Fallout3DummyPluginService( + naggyMockFileWriterService, + naggyMockDummyPluginLogic + ); + ); + } +} diff --git a/libs/bsapacker/tests/Fallout4DummyPluginServiceFacts.cpp b/libs/bsapacker/tests/Fallout4DummyPluginServiceFacts.cpp new file mode 100644 index 0000000..07d7139 --- /dev/null +++ b/libs/bsapacker/tests/Fallout4DummyPluginServiceFacts.cpp @@ -0,0 +1,39 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "Fallout4DummyPluginService.h" +#include "MockFileWriterService.h" +#include "MockDummyPluginLogic.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class Fallout4DummyPluginServiceFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockFileWriterService = new NaggyMock(); + naggyMockDummyPluginLogic = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockFileWriterService; + delete naggyMockDummyPluginLogic; + } + + NaggyMock* naggyMockFileWriterService; + NaggyMock* naggyMockDummyPluginLogic; + }; + + TEST_F(Fallout4DummyPluginServiceFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = Fallout4DummyPluginService( + naggyMockFileWriterService, + naggyMockDummyPluginLogic + ); + ); + } +} diff --git a/libs/bsapacker/tests/FileWriterServiceFacts.cpp b/libs/bsapacker/tests/FileWriterServiceFacts.cpp new file mode 100644 index 0000000..5038640 --- /dev/null +++ b/libs/bsapacker/tests/FileWriterServiceFacts.cpp @@ -0,0 +1,26 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "FileWriterService.h" + +using namespace BsaPacker; + +namespace BsaPackerTests +{ + class FileWriterServiceFacts : public ::testing::Test + { + protected: + void SetUp() override { + } + + void TearDown() override { + } + }; + + TEST_F(FileWriterServiceFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = FileWriterService(); + ); + } +} diff --git a/libs/bsapacker/tests/GeneralArchiveBuilderFacts.cpp b/libs/bsapacker/tests/GeneralArchiveBuilderFacts.cpp new file mode 100644 index 0000000..2139a44 --- /dev/null +++ b/libs/bsapacker/tests/GeneralArchiveBuilderFacts.cpp @@ -0,0 +1,35 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include +#include "MockArchiveBuilderHelper.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class GeneralArchiveBuilderFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockArchiveBuilderHelper = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockArchiveBuilderHelper; + } + + NaggyMock* naggyMockArchiveBuilderHelper; + }; + + TEST_F(GeneralArchiveBuilderFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = GeneralArchiveBuilder( + naggyMockArchiveBuilderHelper, + QDir(), + baNone); + ); + } +} diff --git a/libs/bsapacker/tests/HideLooseAssetServiceFacts.cpp b/libs/bsapacker/tests/HideLooseAssetServiceFacts.cpp new file mode 100644 index 0000000..c868b8d --- /dev/null +++ b/libs/bsapacker/tests/HideLooseAssetServiceFacts.cpp @@ -0,0 +1,33 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "HideLooseAssetService.h" +#include "MockSettingsService.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class HideLooseAssetServiceFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockSettingsService = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockSettingsService; + } + + NaggyMock* naggyMockSettingsService; + }; + + TEST_F(HideLooseAssetServiceFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = HideLooseAssetService( + naggyMockSettingsService); + ); + } +} diff --git a/libs/bsapacker/tests/MockArchiveAutoService.h b/libs/bsapacker/tests/MockArchiveAutoService.h new file mode 100644 index 0000000..ebbbcfa --- /dev/null +++ b/libs/bsapacker/tests/MockArchiveAutoService.h @@ -0,0 +1,10 @@ +#include +#include + +using namespace BsaPacker; + +class MockArchiveAutoService : public IArchiveAutoService +{ +public: + MOCK_METHOD(bool, CreateBSA, (libbsarch::bs_archive_auto *, const QString &, bsa_archive_type_e), (const, override)); +}; diff --git a/libs/bsapacker/tests/MockArchiveBuilderFactory.h b/libs/bsapacker/tests/MockArchiveBuilderFactory.h new file mode 100644 index 0000000..61a7ce4 --- /dev/null +++ b/libs/bsapacker/tests/MockArchiveBuilderFactory.h @@ -0,0 +1,11 @@ +#include +#include + +using namespace BsaPacker; + +class MockArchiveBuilderFactory : public IArchiveBuilderFactory +{ +public: + MOCK_METHOD(std::vector, GetArchiveTypes, (const IModDto*), (const, override)); + MOCK_METHOD(std::unique_ptr, Create, (bsa_archive_type_e, const IModDto*), (const, override)); +}; diff --git a/libs/bsapacker/tests/MockArchiveBuilderHelper.h b/libs/bsapacker/tests/MockArchiveBuilderHelper.h new file mode 100644 index 0000000..632977e --- /dev/null +++ b/libs/bsapacker/tests/MockArchiveBuilderHelper.h @@ -0,0 +1,15 @@ +#include +#include + +using namespace BsaPacker; + +class MockArchiveBuilderHelper : public IArchiveBuilderHelper +{ +public: + MOCK_METHOD(bool, isFileIgnorable, (const std::filesystem::path&, const std::vector&), (const, override)); + MOCK_METHOD(bool, isIncompressible, (const std::filesystem::path&), (const, override)); + MOCK_METHOD(bool, isExtensionBlacklisted, (const std::filesystem::path&), (const, override)); + MOCK_METHOD(uint32_t, getFileCount, (const std::filesystem::path&), (const, override)); + MOCK_METHOD(std::vector, getRootDirectoryFilenames, (const std::filesystem::path&), (const, override)); + MOCK_METHOD(bool, doesPathContainFiles, (const std::filesystem::path&, const std::vector&), (const, override)); +}; diff --git a/libs/bsapacker/tests/MockArchiveNameService.h b/libs/bsapacker/tests/MockArchiveNameService.h new file mode 100644 index 0000000..94dd68b --- /dev/null +++ b/libs/bsapacker/tests/MockArchiveNameService.h @@ -0,0 +1,13 @@ +#include +#include + +using namespace BsaPacker; + +class MockArchiveNameService : public IArchiveNameService +{ +public: + MOCK_METHOD(QString, GetFileExtension, (), (const, override)); + MOCK_METHOD(QString, GetArchiveFullPath, (bsa_archive_type_e, const IModDto *), (const, override)); + MOCK_METHOD(QString, Infix, (bsa_archive_type_e type), (const, override)); + MOCK_METHOD(QString, Suffix, (const QString &pathNoExt), (const, override)); +}; diff --git a/libs/bsapacker/tests/MockDummyPluginLogic.h b/libs/bsapacker/tests/MockDummyPluginLogic.h new file mode 100644 index 0000000..5bdaa50 --- /dev/null +++ b/libs/bsapacker/tests/MockDummyPluginLogic.h @@ -0,0 +1,11 @@ +#include +#include "gmock/gmock.h" + +using namespace BsaPacker; + +class MockDummyPluginLogic : public IDummyPluginLogic +{ +public: + MOCK_METHOD(bool, canCreateDummyESP, (const QString &, const bsa_archive_type_e), (const, override)); + MOCK_METHOD(bool, canCreateDummyESL, (const QString &, const bsa_archive_type_e), (const, override)); +}; diff --git a/libs/bsapacker/tests/MockDummyPluginServiceFactory.h b/libs/bsapacker/tests/MockDummyPluginServiceFactory.h new file mode 100644 index 0000000..36ccc29 --- /dev/null +++ b/libs/bsapacker/tests/MockDummyPluginServiceFactory.h @@ -0,0 +1,10 @@ +#include +#include + +using namespace BsaPacker; + +class MockDummyPluginServiceFactory : public IDummyPluginServiceFactory +{ +public: + MOCK_METHOD(std::unique_ptr, Create, (), (const, override)); +}; diff --git a/libs/bsapacker/tests/MockFileWriterService.h b/libs/bsapacker/tests/MockFileWriterService.h new file mode 100644 index 0000000..164ba5a --- /dev/null +++ b/libs/bsapacker/tests/MockFileWriterService.h @@ -0,0 +1,10 @@ +#include +#include "gmock/gmock.h" + +using namespace BsaPacker; + +class MockFileWriterService : public IFileWriterService +{ +public: + MOCK_METHOD(bool, Write, (const std::string&, const char*, uint32_t), (const, override)); +}; diff --git a/libs/bsapacker/tests/MockHideLooseAssetService.h b/libs/bsapacker/tests/MockHideLooseAssetService.h new file mode 100644 index 0000000..681534d --- /dev/null +++ b/libs/bsapacker/tests/MockHideLooseAssetService.h @@ -0,0 +1,10 @@ +#include +#include + +using namespace BsaPacker; + +class MockHideLooseAssetService : public IHideLooseAssetService +{ +public: + MOCK_METHOD(bool, HideLooseAssets, (const QDir&), (const, override)); +}; diff --git a/libs/bsapacker/tests/MockModContext.h b/libs/bsapacker/tests/MockModContext.h new file mode 100644 index 0000000..fedb3ff --- /dev/null +++ b/libs/bsapacker/tests/MockModContext.h @@ -0,0 +1,13 @@ +#include +#include + +using namespace BsaPacker; + +class MockModContext : public IModContext +{ +public: + MOCK_METHOD(QString, GetAbsoluteModPath, (const QString& modName), (const, override)); + MOCK_METHOD(int, GetNexusId, (), (const, override)); + MOCK_METHOD(QStringList, GetPlugins, (const QDir& modDirectory), (const, override)); + MOCK_METHOD(QStringList, GetValidMods, (), (const, override)); +}; diff --git a/libs/bsapacker/tests/MockModDtoFactory.h b/libs/bsapacker/tests/MockModDtoFactory.h new file mode 100644 index 0000000..d1f0331 --- /dev/null +++ b/libs/bsapacker/tests/MockModDtoFactory.h @@ -0,0 +1,10 @@ +#include +#include + +using namespace BsaPacker; + +class MockModDtoFactory : public IModDtoFactory +{ +public: + MOCK_METHOD(std::unique_ptr, Create, (), (const)); +}; diff --git a/libs/bsapacker/tests/MockOrganizer.h b/libs/bsapacker/tests/MockOrganizer.h new file mode 100644 index 0000000..4c6762e --- /dev/null +++ b/libs/bsapacker/tests/MockOrganizer.h @@ -0,0 +1,61 @@ +#include "imoinfo.h" +#include + +using namespace MOBase; + +class MockOrganizer : public IOrganizer +{ +public: + // clang-format off + MOCK_METHOD(IModRepositoryBridge*, createNexusBridge, (), (const, override)); + MOCK_METHOD(QString, profileName, (), (const, override)); + MOCK_METHOD(QString, profilePath, (), (const, override)); + MOCK_METHOD(QString, downloadsPath, (), (const, override)); + MOCK_METHOD(QString, overwritePath, (), (const, override)); + MOCK_METHOD(QString, basePath, (), (const, override)); + MOCK_METHOD(QString, modsPath, (), (const, override)); + MOCK_METHOD(Version, version, (), (const, override)); + MOCK_METHOD(VersionInfo, appVersion, (), (const, override)); + MOCK_METHOD(IModInterface*, createMod, (GuessedValue &name), (override)); + MOCK_METHOD(IPluginGame*, getGame, (const QString &gameName), (const, override)); + MOCK_METHOD(void, modDataChanged, (IModInterface *mod), (override)); + MOCK_METHOD(QVariant, pluginSetting, (const QString &pluginName, const QString &key), (const, override)); + MOCK_METHOD(void, setPluginSetting, (const QString &pluginName, const QString &key, const QVariant &value), (override)); + MOCK_METHOD(bool, isPluginEnabled, (const QString& pluginName), (const, override)); + MOCK_METHOD(bool, isPluginEnabled, (IPlugin *plugin), (const, override)); + MOCK_METHOD(QVariant, persistent, (const QString &pluginName, const QString &key, const QVariant &def), (const, override)); + MOCK_METHOD(void, setPersistent, (const QString &pluginName, const QString &key, const QVariant &value, bool sync), (override)); + MOCK_METHOD(QString, pluginDataPath, (), (const, override)); + MOCK_METHOD(IModInterface*, installMod, (const QString &fileName, const QString &nameSuggestion), (override)); + MOCK_METHOD(QString, resolvePath, (const QString &fileName), (const, override)); + MOCK_METHOD(QStringList, listDirectories, (const QString &directoryName), (const, override)); + MOCK_METHOD(QStringList, findFiles, (const QString &path, const std::function &filter), (const, override)); + MOCK_METHOD(QStringList, findFiles, (const QString &path, const QStringList &filter), (const, override)); + MOCK_METHOD(QStringList, getFileOrigins, (const QString &fileName) ,(const, override)); + MOCK_METHOD(QList, findFileInfos, (const QString &path, const std::function &filter), (const, override)); + MOCK_METHOD(std::shared_ptr, virtualFileTree, (), (const, override)); + MOCK_METHOD(MOBase::IDownloadManager*, downloadManager, (), (const, override)); + MOCK_METHOD(MOBase::IPluginList*, pluginList, (), (const, override)); + MOCK_METHOD(MOBase::IModList*, modList, (), (const, override)); + MOCK_METHOD(MOBase::IProfile*, profile, (), (const, override)); + MOCK_METHOD(IGameFeatures*, gameFeatures, (), (const, override)); + MOCK_METHOD(HANDLE, startApplication, (const QString &executable, const QStringList &args, const QString &cwd, const QString &profile, const QString &forcedCustomOverwrite, bool ignoreCustomOverwrite), (override)); + MOCK_METHOD(bool, waitForApplication, (HANDLE handle, bool refresh, LPDWORD exitCode), (const, override)); + MOCK_METHOD(bool, onAboutToRun, (const std::function &func), (override)); + MOCK_METHOD(bool, onAboutToRun, (const std::function&), (override)); + MOCK_METHOD(bool, onFinishedRun, (const std::function &func), (override)); + MOCK_METHOD(void, refresh, (bool saveChanges), (override)); + MOCK_METHOD(MOBase::IPluginGame const *, managedGame, (), (const, override)); + MOCK_METHOD(bool, onUserInterfaceInitialized, (const std::function &), (override)); + MOCK_METHOD(bool, onNextRefresh, (const std::function &, bool), (override)); + MOCK_METHOD(bool, onProfileCreated, (const std::function&), (override)); + MOCK_METHOD(bool, onProfileRemoved, (const std::function&), (override)); + MOCK_METHOD(bool, onProfileRenamed, (const std::function&), (override)); + MOCK_METHOD(bool, onProfileChanged, (const std::function &), (override)); + MOCK_METHOD(bool, onPluginSettingChanged, (const std::function &), (override)); + MOCK_METHOD(bool, onPluginEnabled, (const std::function&), (override)); + MOCK_METHOD(bool, onPluginEnabled, (const QString&, const std::function&), (override)); + MOCK_METHOD(bool, onPluginDisabled, (const std::function&), (override)); + MOCK_METHOD(bool, onPluginDisabled, (const QString&, const std::function&), (override)); + // clang-format on +}; diff --git a/libs/bsapacker/tests/MockOverrideFileService.h b/libs/bsapacker/tests/MockOverrideFileService.h new file mode 100644 index 0000000..b7c0414 --- /dev/null +++ b/libs/bsapacker/tests/MockOverrideFileService.h @@ -0,0 +1,10 @@ +#include +#include + +using namespace BsaPacker; + +class MockOverrideFileService : public IOverrideFileService +{ +public: + MOCK_METHOD(bool, CreateOverrideFile, (const int nexusId, const QString &modPath, const QStringList &archiveNames), (const, override)); +}; diff --git a/libs/bsapacker/tests/MockPackerDialog.h b/libs/bsapacker/tests/MockPackerDialog.h new file mode 100644 index 0000000..aafad52 --- /dev/null +++ b/libs/bsapacker/tests/MockPackerDialog.h @@ -0,0 +1,17 @@ +#include +#include + +using namespace BsaPacker; + +class MockPackerDialog : public IPackerDialog +{ +public: + MOCK_METHOD(bool, IsNewFilename, (), (const, override)); + MOCK_METHOD(QString, SelectedMod, (), (const, override)); + MOCK_METHOD(void, RefreshModList, (), (override)); + MOCK_METHOD(QString, SelectedPluginItem, (), (const, override)); + MOCK_METHOD(void, UpdateNameList, (const QString&), (override)); + MOCK_METHOD(void, RefreshSelectedName, (), (override)); + MOCK_METHOD(int, Exec, (), (override)); + MOCK_METHOD(void, RefreshOkButton, (), (override)); +}; diff --git a/libs/bsapacker/tests/MockSettingsService.h b/libs/bsapacker/tests/MockSettingsService.h new file mode 100644 index 0000000..73bfea0 --- /dev/null +++ b/libs/bsapacker/tests/MockSettingsService.h @@ -0,0 +1,10 @@ +#include +#include + +using namespace BsaPacker; + +class MockSettingsService : public ISettingsService +{ +public: + MOCK_METHOD(QVariant, GetPluginSetting, (const QString&), (const, override)); +}; diff --git a/libs/bsapacker/tests/ModContextFacts.cpp b/libs/bsapacker/tests/ModContextFacts.cpp new file mode 100644 index 0000000..3ab1808 --- /dev/null +++ b/libs/bsapacker/tests/ModContextFacts.cpp @@ -0,0 +1,33 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "ModContext.h" +#include "MockOrganizer.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class ModContextFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockOrganizer = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockOrganizer; + } + + NaggyMock* naggyMockOrganizer; + }; + + TEST_F(ModContextFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = ModContext( + naggyMockOrganizer); + ); + } +} diff --git a/libs/bsapacker/tests/ModDtoFactoryFacts.cpp b/libs/bsapacker/tests/ModDtoFactoryFacts.cpp new file mode 100644 index 0000000..ae9a9f7 --- /dev/null +++ b/libs/bsapacker/tests/ModDtoFactoryFacts.cpp @@ -0,0 +1,53 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include +#include "MockModContext.h" +#include "MockPackerDialog.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; +using ::testing::Return; + +namespace BsaPackerTests +{ + class ModDtoFactoryFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockModContext = new NaggyMock(); + naggyMockPackerDialog = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockModContext; + delete naggyMockPackerDialog; + } + + NaggyMock* naggyMockModContext; + NaggyMock* naggyMockPackerDialog; + }; + + TEST_F(ModDtoFactoryFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW(ModDtoFactory( + naggyMockModContext, + naggyMockPackerDialog)); + } + + TEST_F(ModDtoFactoryFacts, Create_WhenGameIsNewVegas_ReturnsNewVegasIdInDto) + { + // Arrange + auto sut = ModDtoFactory( + naggyMockModContext, + naggyMockPackerDialog); + ON_CALL(*naggyMockModContext, GetNexusId()) + .WillByDefault(Return(1)); + ON_CALL(*naggyMockPackerDialog, Exec()) + .WillByDefault(Return(QDialog::DialogCode::Accepted)); + + auto result = sut.Create(); + + EXPECT_EQ(1, result->NexusId()); + } +} diff --git a/libs/bsapacker/tests/NewVegasDummyPluginServiceFacts.cpp b/libs/bsapacker/tests/NewVegasDummyPluginServiceFacts.cpp new file mode 100644 index 0000000..98216ff --- /dev/null +++ b/libs/bsapacker/tests/NewVegasDummyPluginServiceFacts.cpp @@ -0,0 +1,39 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "NewVegasDummyPluginService.h" +#include "MockFileWriterService.h" +#include "MockDummyPluginLogic.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class NewVegasDummyPluginServiceFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockFileWriterService = new NaggyMock(); + naggyMockDummyPluginLogic = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockFileWriterService; + delete naggyMockDummyPluginLogic; + } + + NaggyMock* naggyMockFileWriterService; + NaggyMock* naggyMockDummyPluginLogic; + }; + + TEST_F(NewVegasDummyPluginServiceFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = NewVegasDummyPluginService( + naggyMockFileWriterService, + naggyMockDummyPluginLogic + ); + ); + } +} diff --git a/libs/bsapacker/tests/OblivionDummyPluginServiceFacts.cpp b/libs/bsapacker/tests/OblivionDummyPluginServiceFacts.cpp new file mode 100644 index 0000000..dbebbb1 --- /dev/null +++ b/libs/bsapacker/tests/OblivionDummyPluginServiceFacts.cpp @@ -0,0 +1,39 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "OblivionDummyPluginService.h" +#include "MockFileWriterService.h" +#include "MockDummyPluginLogic.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class OblivionDummyPluginServiceFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockFileWriterService = new NaggyMock(); + naggyMockDummyPluginLogic = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockFileWriterService; + delete naggyMockDummyPluginLogic; + } + + NaggyMock* naggyMockFileWriterService; + NaggyMock* naggyMockDummyPluginLogic; + }; + + TEST_F(OblivionDummyPluginServiceFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = OblivionDummyPluginService( + naggyMockFileWriterService, + naggyMockDummyPluginLogic + ); + ); + } +} diff --git a/libs/bsapacker/tests/SkyrimDummyPluginServiceFacts.cpp b/libs/bsapacker/tests/SkyrimDummyPluginServiceFacts.cpp new file mode 100644 index 0000000..e699c10 --- /dev/null +++ b/libs/bsapacker/tests/SkyrimDummyPluginServiceFacts.cpp @@ -0,0 +1,39 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "SkyrimDummyPluginService.h" +#include "MockFileWriterService.h" +#include "MockDummyPluginLogic.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class SkyrimDummyPluginServiceFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockFileWriterService = new NaggyMock(); + naggyMockDummyPluginLogic = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockFileWriterService; + delete naggyMockDummyPluginLogic; + } + + NaggyMock* naggyMockFileWriterService; + NaggyMock* naggyMockDummyPluginLogic; + }; + + TEST_F(SkyrimDummyPluginServiceFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = SkyrimDummyPluginService( + naggyMockFileWriterService, + naggyMockDummyPluginLogic + ); + ); + } +} diff --git a/libs/bsapacker/tests/SkyrimSEDummyPluginServiceFacts.cpp b/libs/bsapacker/tests/SkyrimSEDummyPluginServiceFacts.cpp new file mode 100644 index 0000000..5a67937 --- /dev/null +++ b/libs/bsapacker/tests/SkyrimSEDummyPluginServiceFacts.cpp @@ -0,0 +1,39 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "SkyrimSEDummyPluginService.h" +#include "MockFileWriterService.h" +#include "MockDummyPluginLogic.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class SkyrimSEDummyPluginServiceFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockFileWriterService = new NaggyMock(); + naggyMockDummyPluginLogic = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockFileWriterService; + delete naggyMockDummyPluginLogic; + } + + NaggyMock* naggyMockFileWriterService; + NaggyMock* naggyMockDummyPluginLogic; + }; + + TEST_F(SkyrimSEDummyPluginServiceFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = SkyrimSEDummyPluginService( + naggyMockFileWriterService, + naggyMockDummyPluginLogic + ); + ); + } +} diff --git a/libs/bsapacker/tests/TextureArchiveBuilderFacts.cpp b/libs/bsapacker/tests/TextureArchiveBuilderFacts.cpp new file mode 100644 index 0000000..3ada0de --- /dev/null +++ b/libs/bsapacker/tests/TextureArchiveBuilderFacts.cpp @@ -0,0 +1,35 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include +#include "MockArchiveBuilderHelper.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class TextureArchiveBuilderFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockArchiveBuilderHelper = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockArchiveBuilderHelper; + } + + NaggyMock* naggyMockArchiveBuilderHelper; + }; + + TEST_F(TextureArchiveBuilderFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = TextureArchiveBuilder( + naggyMockArchiveBuilderHelper, + QDir(), + baNone); + ); + } +} diff --git a/libs/bsapacker/tests/TexturelessArchiveBuilderFacts.cpp b/libs/bsapacker/tests/TexturelessArchiveBuilderFacts.cpp new file mode 100644 index 0000000..f0aa7d2 --- /dev/null +++ b/libs/bsapacker/tests/TexturelessArchiveBuilderFacts.cpp @@ -0,0 +1,35 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include +#include "MockArchiveBuilderHelper.h" + +using namespace BsaPacker; +using ::testing::NaggyMock; + +namespace BsaPackerTests +{ + class TexturelessArchiveBuilderFacts : public ::testing::Test + { + protected: + void SetUp() override { + naggyMockArchiveBuilderHelper = new NaggyMock(); + } + + void TearDown() override { + delete naggyMockArchiveBuilderHelper; + } + + NaggyMock* naggyMockArchiveBuilderHelper; + }; + + TEST_F(TexturelessArchiveBuilderFacts, Ctor_Always_Constructs) + { + EXPECT_NO_THROW( + auto result = TexturelessArchiveBuilder( + naggyMockArchiveBuilderHelper, + QDir(), + baNone); + ); + } +} diff --git a/libs/bsapacker/vcpkg.json b/libs/bsapacker/vcpkg.json new file mode 100644 index 0000000..addd280 --- /dev/null +++ b/libs/bsapacker/vcpkg.json @@ -0,0 +1,34 @@ +{ + "dependencies": ["mo2-libbsarch", "mo2-dds-header", "directxtex", "bext-di"], + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake"] + }, + "testing": { + "description": "Build BSA Packer tests.", + "dependencies": ["gtest"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43", + "packages": ["boost*", "boost-*", "bext-*"] + }, + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "9936967e7c0ba1968e54a2fa6e6ad27b4f992b9b", + "packages": ["mo2-*"] + } + ] + } +} diff --git a/libs/bsatk/.clang-format b/libs/bsatk/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/bsatk/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/bsatk/.gitattributes b/libs/bsatk/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/bsatk/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/bsatk/.github/workflows/build.yml b/libs/bsatk/.github/workflows/build.yml new file mode 100644 index 0000000..fdad926 --- /dev/null +++ b/libs/bsatk/.github/workflows/build.yml @@ -0,0 +1,40 @@ +name: Build BSATK + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }} + +jobs: + build: + runs-on: windows-2022 + steps: + # set VCPKG Root + - name: "Set environmental variables" + shell: bash + run: | + echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> $GITHUB_ENV + + # checkout + - uses: actions/checkout@v4 + + - name: Configure BSATK build + shell: pwsh + run: | + cmake --preset vs2022-windows "-DCMAKE_INSTALL_PREFIX=install" + + - name: Build BSATK + run: cmake --build vsbuild --config RelWithDebInfo + + - name: Install BSATK + run: cmake --install vsbuild --config RelWithDebInfo + + - name: Upload BSATK artifact + uses: actions/upload-artifact@master + with: + name: BSATK + path: ./install diff --git a/libs/bsatk/.github/workflows/linting.yml b/libs/bsatk/.github/workflows/linting.yml new file mode 100644 index 0000000..6f01385 --- /dev/null +++ b/libs/bsatk/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint BSATK + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/bsatk/.gitignore b/libs/bsatk/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/bsatk/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/bsatk/.pre-commit-config.yaml b/libs/bsatk/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/bsatk/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/bsatk/CMakeFiles/CMakeSystem.cmake b/libs/bsatk/CMakeFiles/CMakeSystem.cmake new file mode 100644 index 0000000..c9c4e81 --- /dev/null +++ b/libs/bsatk/CMakeFiles/CMakeSystem.cmake @@ -0,0 +1,15 @@ +set(CMAKE_HOST_SYSTEM "Linux-6.18.9-2-cachyos") +set(CMAKE_HOST_SYSTEM_NAME "Linux") +set(CMAKE_HOST_SYSTEM_VERSION "6.18.9-2-cachyos") +set(CMAKE_HOST_SYSTEM_PROCESSOR "x86_64") + + + +set(CMAKE_SYSTEM "Linux-6.18.9-2-cachyos") +set(CMAKE_SYSTEM_NAME "Linux") +set(CMAKE_SYSTEM_VERSION "6.18.9-2-cachyos") +set(CMAKE_SYSTEM_PROCESSOR "x86_64") + +set(CMAKE_CROSSCOMPILING "FALSE") + +set(CMAKE_SYSTEM_LOADED 1) diff --git a/libs/bsatk/CMakeLists.txt b/libs/bsatk/CMakeLists.txt new file mode 100644 index 0000000..571ce1f --- /dev/null +++ b/libs/bsatk/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.16) + +include(CMakePackageConfigHelpers) + +project(bsatk) + +add_subdirectory(src) + +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/mo2-bsatk-config.cmake" + INSTALL_DESTINATION "lib/cmake/mo2-bsatk" + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO +) + +include(${CMAKE_CURRENT_SOURCE_DIR}/Version.cmake) +set(BSATK_VERSION + ${BSATK_MAJOR_VERSION}.${BSATK_MINOR_VERSION}.${BSATK_PATCH_VERSION}) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/mo2-bsatk-config-version.cmake" + VERSION "${BSATK_VERSION}" + COMPATIBILITY AnyNewerVersion +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/mo2-bsatk-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/mo2-bsatk-config-version.cmake + DESTINATION lib/cmake/mo2-bsatk +) diff --git a/libs/bsatk/CMakePresets.json b/libs/bsatk/CMakePresets.json new file mode 100644 index 0000000..cc009b3 --- /dev/null +++ b/libs/bsatk/CMakePresets.json @@ -0,0 +1,57 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/bsatk/README.md b/libs/bsatk/README.md new file mode 100644 index 0000000..c5fe1e2 --- /dev/null +++ b/libs/bsatk/README.md @@ -0,0 +1,3 @@ +[![Build status](https://ci.appveyor.com/api/projects/status/6g5v1r0v345m1iy9?svg=true)](https://ci.appveyor.com/project/Modorganizer2/modorganizer-bsatk) + +# modorganizer-bsatk diff --git a/libs/bsatk/Version.cmake b/libs/bsatk/Version.cmake new file mode 100644 index 0000000..e38ed2b --- /dev/null +++ b/libs/bsatk/Version.cmake @@ -0,0 +1,3 @@ +set(BSATK_MAJOR_VERSION 1) +set(BSATK_MINOR_VERSION 4) +set(BSATK_PATCH_VERSION 1) diff --git a/libs/bsatk/cmake/config.cmake.in b/libs/bsatk/cmake/config.cmake.in new file mode 100644 index 0000000..ae33254 --- /dev/null +++ b/libs/bsatk/cmake/config.cmake.in @@ -0,0 +1,10 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +find_dependency(Boost CONFIG COMPONENTS thread interprocess) +find_dependency(ZLIB) +find_dependency(lz4 CONFIG) +find_dependency(mo2-dds-header CONFIG) + +include ( "${CMAKE_CURRENT_LIST_DIR}/mo2-bsatk-targets.cmake" ) diff --git a/libs/bsatk/include/bsatk/bsaarchive.h b/libs/bsatk/include/bsatk/bsaarchive.h new file mode 100644 index 0000000..cd15f64 --- /dev/null +++ b/libs/bsatk/include/bsatk/bsaarchive.h @@ -0,0 +1,244 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 BSA_ARCHIVE_H +#define BSA_ARCHIVE_H + +#include +#include +#include +#include + +#include + +#include + +#include "bsafolder.h" +#include "bsatypes.h" +#include "errorcodes.h" +namespace boost +{ +class mutex; +namespace interprocess +{ + class interprocess_semaphore; +} +} // namespace boost + +namespace BSA +{ + +class File; + +/** + * @brief top level structure to represent a bsa file + */ +class Archive +{ +public: + using DataBuffer = std::pair, BSAULong>; + +private: + static const unsigned int FLAG_HASDIRNAMES = 0x00000001; + static const unsigned int FLAG_HASFILENAMES = 0x00000002; + static const unsigned int FLAG_DEFAULTCOMPRESSED = 0x00000004; + static const unsigned int FLAG_NAMEPREFIXED = + 0x00000100; // if set, the full file name is prefixed before a data block + + /* Record flags */ +#define OB_BSAFILE_FLAG_COMPRESS \ + 0xC0000000 //!< Bit mask with OBBSAFileInfo::sizeFlags to get the compression status + +public: + /** + * constructor + */ + Archive(); + ~Archive(); + /** + * read the archive from file + * @param fileName name of the file to read from + * @param testHashes if true, the hashes of file names will be checked to ensure the + * file is valid. This can be skipped for performance reasons + * @return ERROR_NONE on success or an error code + */ + EErrorCode read(const char* fileName, bool testHashes); + /** + * write the archive to disc + * @param fileName name of the file to write to + * @return ERROR_NONE on success or an error code + */ + EErrorCode write(const char* fileName); + /** + * @brief close the archive + */ + void close(); + /** + * change the archive type + * @param type new archive type + */ + void setType(ArchiveType type) { m_Type = type; } + /** + * @return type of the archive (supported game) + */ + ArchiveType getType() const { return m_Type; } + /** + * retrieve top-level folder + * @return descriptor of the root folder + */ + Folder::Ptr getRoot() { return m_RootFolder; } + /** + * extract a file from the archive + * @param file descriptor of the file to extract + * @param outputDirectory name of the directory to extract to. + * may be absolute or relative + * @return ERROR_NONE on success or an error code + */ + EErrorCode extract(File::Ptr file, const char* outputDirectory) const; + /** + * @return archive flags + */ + BSAULong getFlags() const { return m_ArchiveFlags; } + + /** + * extract all files. this is potentially faster than iterating over all files and + * extracting each + * @param outputDirectory name of the directory to extract to. + * may be absolute or relative + * @param progress callback function called on progress + * @param overwrite if true (default) files are overwritten if they exist + * @return ERROR_NONE on success or an error code + */ + EErrorCode + extractAll(const char* outputDirectory, + const std::function& progress, + bool overwrite = true); + + /** + * @param file the file to check + * @return true if the file is compressed, false otherwise + */ + bool compressed(const File::Ptr& file) const; + /** + * create a new file to be placed in this archive. The new file is NOT + * added to a folder, use BSA::Folder::addFile for that + * @param name name of the file to be used inside the archive + * @param sourceName filename path to the file to add + * @param compressed true if the file should be compressed (not supported yet!) + * @return pointer to the new file + */ + File::Ptr createFile(const std::string& name, const std::string& sourceName, + bool compressed); + +private: + struct Header + { + uint32_t fileIdentifier; + char archType[5]; + ArchiveType type; + BSAUInt offset; + BSAUInt archiveFlags; + BSAUInt folderCount; + BSAUInt fileCount; + BSAUInt folderNameLength; + BSAUInt fileNameLength; + BSAUInt fileFlags; + BSAHash nameTableOffset; + }; + + struct FileInfo + { + File::Ptr file; + DataBuffer data; + }; + +private: + static Header readHeader(std::fstream& infile); + + static ArchiveType typeFromID(BSAULong typeID); + + static std::shared_ptr decompress(unsigned char* inBuffer, + BSAULong inSize, + EErrorCode& result, + BSAULong& outSize); + + BSAULong typeToID(ArchiveType type); + + Folder readFolderRecord(std::fstream& file); + + // EErrorCode extractDirect(const File &fileInfo, std::ofstream &outFile); + // EErrorCode extractCompressed(const File &fileInfo, std::ofstream &outFile); + + bool defaultCompressed() const { return m_ArchiveFlags & FLAG_DEFAULTCOMPRESSED; } + // starting with FO3 the bsa may prefix the file name to the file blob if archive flag + // 0x100 is set + bool namePrefixed() const + { + return (m_Type != TYPE_OBLIVION) && ((m_ArchiveFlags & FLAG_NAMEPREFIXED) != 0); + } + + BSAULong countFiles() const; + + std::vector collectFolderNames() const; + std::vector collectFileNames() const; + + BSAULong countCharacters(const std::vector& list) const; + BSAULong determineFileFlags(const std::vector& fileList) const; + + void writeHeader(std::fstream& outfile, BSAULong fileFlags, BSAULong numFolders, + BSAULong folderNamesLength, BSAULong fileNamesLength); + + DirectX::DDS_HEADER getDDSHeader(File::Ptr file, + DirectX::DDS_HEADER_DXT10& DX10Header, + bool& isDX10) const; + void getDX10Header(DirectX::DDS_HEADER_DXT10& DX10Header, File::Ptr file, + DirectX::DDS_HEADER DDSHeader) const; + + EErrorCode extractDirect(File::Ptr file, std::ofstream& outFile) const; + EErrorCode extractCompressed(File::Ptr file, std::ofstream& outFile) const; + + void createFolders(const std::string& targetDirectory, Folder::Ptr folder); + + void readFiles(std::queue& queue, boost::mutex& mutex, + boost::interprocess::interprocess_semaphore& bufferCount, + boost::interprocess::interprocess_semaphore& queueFree, + std::vector::iterator begin, + std::vector::iterator end); + + void extractFiles(const std::string& targetDirectory, std::queue& queue, + boost::mutex& mutex, + boost::interprocess::interprocess_semaphore& bufferCount, + boost::interprocess::interprocess_semaphore& queueFree, + int totalFiles, bool overwrite, int& filesDone); + + void cleanFolder(Folder::Ptr folder); + +private: + mutable std::fstream m_File; + + Folder::Ptr m_RootFolder; + + BSAULong m_ArchiveFlags; + ArchiveType m_Type; +}; + +} // namespace BSA + +#endif // BSA_ARCHIVE_H diff --git a/libs/bsatk/include/bsatk/bsaexception.h b/libs/bsatk/include/bsatk/bsaexception.h new file mode 100644 index 0000000..1624f4a --- /dev/null +++ b/libs/bsatk/include/bsatk/bsaexception.h @@ -0,0 +1,51 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 BSAEXCEPTION_H +#define BSAEXCEPTION_H + +#include + +/** + * construct a string from a printf-style format + * @param format printf-style format to create the string from + * @param ... variable parameter list + * @return the constructed string + */ +std::string makeString(const char* format, ...); + +/** + * custom exception to be thrown when invalid data is encountered + */ +class data_invalid_exception : public std::exception +{ + +public: + explicit data_invalid_exception(const std::string& message); + + virtual ~data_invalid_exception() noexcept {} + + virtual const char* what() const noexcept { return m_Message.c_str(); } + +private: + std::string m_Message; +}; + +#endif // BSAEXCEPTION_H diff --git a/libs/bsatk/include/bsatk/bsafile.h b/libs/bsatk/include/bsatk/bsafile.h new file mode 100644 index 0000000..64fbf85 --- /dev/null +++ b/libs/bsatk/include/bsatk/bsafile.h @@ -0,0 +1,145 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 BSAFILE_H +#define BSAFILE_H + +#include +#include +#include + +#include "errorcodes.h" +#include "filehash.h" + +namespace BSA +{ + +class Folder; + +class File +{ + + friend class Folder; + friend class Archive; + +public: + typedef std::shared_ptr Ptr; + friend bool ByOffset(const File::Ptr& LHS, const File::Ptr& RHS); + +private: + static const unsigned int SIZEMASK = 0x3fffffff; + static const unsigned int COMPRESSMASK = 0xC0000000; + +public: + /** + * @return the name of the file + */ + const std::string& getName() const { return m_Name; } + /** + * @return full path of this file within the archive + */ + std::string getFilePath() const; + /** + * @return size of the file. If the source is an archive and the file is + * compressed, this returns the compressed size! + */ + BSAULong getFileSize() const { return m_FileSize; } + + BSAULong getUncompressedFileSize() const { return m_UncompressedFileSize; } + +private: + // copy constructor not implemented + File(const File& reference); + + // assignment operator not implemented + File& operator=(const File& reference); + + /** + * construct file from source archive + * @param file a file read from + * @param folder the folder to add the file to + * @param type the archive type we're parsing + */ + File(std::fstream& file, Folder* folder); + + /** + * construct file from morrowind BSA or BA2 + * @param name of the base file from source archive + * @param folder the folder to add the file to + * @param fileSize the file size of the file in the archive + * @param dataOffset the offset of the file data in the archive + * @param toggleCompressed the detected compression mode of the archive + */ + File(const std::string& name, Folder* folder, BSAULong fileSizee, BSAHash dataOffset, + BSAULong uncompressedFileSize, FO4TextureHeader header, + std::vector& texChunks); + + /** + * construct from loose file + * @param name the base name of the file inside the archive + * @param sourceFile the file to read from + * @param folder the folder to add the file to + * @param toggleCompressed if true, the default compression mode of the + * archive is overwritten + */ + File(const std::string& name, const std::string& sourceFile, Folder* folder, + bool toggleCompressed); + + /** + * @return true if its compression mode for this file differs from the archive default + */ + bool compressToggled() const { return m_ToggleCompressed; } + + /** + * @return offset to the file data. Only valid if the source of the file is + * an archive + */ + BSAHash getDataOffset() const { return m_DataOffset; } + void writeHeader(std::fstream& file) const; + EErrorCode writeData(std::fstream& sourceArchive, std::fstream& targetArchive) const; + + void setFileSize(BSAULong fileSize) { m_FileSize = fileSize; } + + void readFileName(std::fstream& file, bool testHashes); + +private: + Folder* m_Folder; + bool m_New; + + BSAHash m_NameHash; + std::string m_Name; + mutable BSAULong m_FileSize; + mutable BSAULong m_UncompressedFileSize; + BSAHash m_DataOffset; + bool m_ToggleCompressed; + + FO4TextureHeader m_TextureHeader = {}; + std::vector m_TextureChunks; + + std::string m_SourceFile; + bool m_ToggleCompressedWrite; + mutable BSAULong m_DataOffsetWrite; +}; + +extern bool ByOffset(const File::Ptr& LHS, const File::Ptr& RHS); + +} // namespace BSA + +#endif // BSAFILE_H diff --git a/libs/bsatk/include/bsatk/bsafolder.h b/libs/bsatk/include/bsatk/bsafolder.h new file mode 100644 index 0000000..f1afa29 --- /dev/null +++ b/libs/bsatk/include/bsatk/bsafolder.h @@ -0,0 +1,170 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 BSAFOLDER_H +#define BSAFOLDER_H + +#include "bsafile.h" +#include "bsatypes.h" +#include "errorcodes.h" +#include +#include +#include +#include + +namespace BSA +{ + +class Folder +{ + + friend class Archive; + +public: + typedef std::shared_ptr Ptr; + +public: + /** + * @return name of this folder + */ + const std::string& getName() const { return m_Name; } + /** + * @return full path to this folder + */ + std::string getFullPath() const; + /** + * @return the number of subfolders within this folder + */ + unsigned int getNumSubFolders() const + { + return static_cast(m_SubFolders.size()); + } + /** + * @param index index of a subfolder within this folder + * @return a descriptor for the subfolder + * @throw out_of_range this will throw an exception if the index is invalid + */ + const Folder::Ptr getSubFolder(unsigned int index) const; + /** + * @return the number of files in this folder + */ + unsigned int getNumFiles() const { return static_cast(m_Files.size()); } + /** + * @return the number of files in this folder and subfolder + */ + unsigned int countFiles() const; + /** + * @param index index of a file in this folder + * @return a descriptor for the file + * @throw out_of_range this will throw an exception if the index is invalid + */ + const File::Ptr getFile(unsigned int index) const; + /** + * adds a new file to the folder + * @param file the new file to add + */ + void addFile(const File::Ptr& file) { m_Files.push_back(file); } + /** + * add an empty folder as a subfolder to this one. + * @param folderName name of the new folder + * @return pointer to the new folder + * @note this folder will not be written to the bsa if it has no content + */ + Folder::Ptr addFolder(const std::string& folderName); + +private: + /** + * construct a new, empty, folder + */ + Folder(); + + // copy constructor not implemented + Folder(const Folder& reference); + + // assignment operator - not implemented + Folder& operator=(const Folder& reference); + + /** + * factory function to read a folder object from disc. This also reads part of the + * information about the files within + * @param file file stream to read from, already placed at the correct position + * @param fileNamesLength length of the file names list. This is required to correctly + * calculate offsets + * @param endPos position inside file where the last folder header ends. This is + * updated by the constructor so that it is the correct value after all + * folders are read + * @return the new Folder object + */ + Folder::Ptr readFolder(std::fstream& file, BSAUInt fileNamesLength, BSAUInt& endPos); + + Folder::Ptr readFolderSE(std::fstream& file, BSAUInt fileNamesLength, + BSAUInt& endPos); + + /** + * recursive function to determine the correct subfolder to place the new + * folder in + */ + void addFolderInt(Folder::Ptr folder); + + /** + * recursive function that returns an existing folder match or generates the structure + * for a new folder + * @param folder folder to find or create + * @return the final determined / generated Folder + */ + Folder::Ptr addOrFindFolderInt(Folder* folder); + + /** + * Add a new folder to the structure. + * It will automatically be added to the correct sub-folder if applicable. + */ + Folder::Ptr addFolder(std::fstream& file, BSAUInt fileNamesLength, BSAUInt& endPos, + ArchiveType type); + + Folder::Ptr addFolderFromFile(std::string filePath, BSAUInt size, BSAHash offset, + BSAUInt uncompressedSize, FO4TextureHeader header, + std::vector& texChunks); + + bool resolveFileNames(std::fstream& file, bool testHashes); + + void writeHeader(std::fstream& file) const; + void writeData(std::fstream& file, BSAULong fileNamesLength) const; + EErrorCode writeFileData(std::fstream& sourceFile, std::fstream& targetFile) const; + void collectFolders(std::vector& folderList) const; + void collectFiles(std::vector& fileList) const; + void collectFileNames(std::vector& nameList) const; + void collectFolderNames(std::vector& nameList) const; + +private: + Folder* m_Parent; + BSAHash m_NameHash; + std::string m_Name; + BSAULong m_FileCount; + BSAHash m_Offset; + std::vector m_SubFolders; + std::unordered_map m_SubFoldersByName; + std::vector m_Files; + + mutable BSAULong m_OffsetWrite; +}; + +} // namespace BSA + +#endif /* BSAFOLDER_H */ diff --git a/libs/bsatk/include/bsatk/bsatk.h b/libs/bsatk/include/bsatk/bsatk.h new file mode 100644 index 0000000..330a240 --- /dev/null +++ b/libs/bsatk/include/bsatk/bsatk.h @@ -0,0 +1,15 @@ +/* + * File: bsatk.h + * Author: tannin + * + * Created on March 16, 2012, 10:35 AM + */ + +#ifndef BSATK_H +#define BSATK_H + +#include "bsaarchive.h" +#include "bsafile.h" +#include "bsafolder.h" + +#endif /* BSATK_H */ diff --git a/libs/bsatk/include/bsatk/bsatypes.h b/libs/bsatk/include/bsatk/bsatypes.h new file mode 100644 index 0000000..27dbdf5 --- /dev/null +++ b/libs/bsatk/include/bsatk/bsatypes.h @@ -0,0 +1,133 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 BSATYPES_H +#define BSATYPES_H + +#include +#include + +#include + +#include + +#include "bsaexception.h" + +#ifdef WIN32 +#include + +typedef unsigned char BSAUChar; +typedef unsigned short BSAUShort; +typedef unsigned int BSAUInt; +typedef unsigned long BSAULong; +typedef unsigned long long BSAHash; + +#else // WIN32 +#include + +typedef unsigned char BSAUChar; +typedef unsigned short BSAUShort; +typedef unsigned int BSAUInt; +typedef unsigned long BSAULong; +typedef unsigned long long BSAHash; + +#endif // WIN32 + +enum ArchiveType +{ + TYPE_MORROWIND, + TYPE_OBLIVION, + TYPE_FALLOUT3, + TYPE_FALLOUTNV = TYPE_FALLOUT3, + TYPE_SKYRIM = TYPE_FALLOUT3, + TYPE_SKYRIMSE, + TYPE_FALLOUT4, + TYPE_STARFIELD, + TYPE_STARFIELD_LZ4_TEXTURE, + TYPE_FALLOUT4NG_7, + TYPE_FALLOUT4NG_8 +}; + +struct MorrowindFileOffset +{ + BSAUInt size; + BSAUInt offset; +}; + +struct FO4TextureHeader +{ + BSAUInt nameHash; + char extension[4]; + BSAUInt dirHash; + BSAUChar unknown1; + BSAUChar chunkNumber; + BSAUShort chunkHeaderSize; + BSAUShort height; + BSAUShort width; + BSAUChar mipCount; + DXGI_FORMAT format; + bool isCubemap; + BSAUChar unknown2; +}; + +struct FO4TextureChunk +{ + BSAHash offset; + BSAUInt packedSize; + BSAUInt unpackedSize; + BSAUShort startMip; + BSAUShort endMip; + BSAUInt unknown; +}; + +template +static T readType(std::fstream& file) +{ + union + { + char buffer[sizeof(T)]; + T value; + }; + if (!file.read(buffer, sizeof(T))) { + throw data_invalid_exception("can't read from bsa"); + } + return value; +} + +template +static void writeType(std::fstream& file, const T& value) +{ + union + { + char buffer[sizeof(T)]; + T valueTemp; + }; + valueTemp = value; + + file.write(buffer, sizeof(T)); +} + +std::string readBString(std::fstream& file); +void writeBString(std::fstream& file, const std::string& string); + +std::string readZString(std::fstream& file); +void writeZString(std::fstream& file, const std::string& string); + +#endif // BSATYPES_H diff --git a/libs/bsatk/include/bsatk/errorcodes.h b/libs/bsatk/include/bsatk/errorcodes.h new file mode 100644 index 0000000..f2780db --- /dev/null +++ b/libs/bsatk/include/bsatk/errorcodes.h @@ -0,0 +1,41 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 ERRORCODES_H +#define ERRORCODES_H + +namespace BSA +{ + +enum EErrorCode +{ + ERROR_NONE, + ERROR_INVALIDHASHES, + ERROR_FILENOTFOUND, + ERROR_INVALIDDATA, + ERROR_ACCESSFAILED, + ERROR_ZLIBINITFAILED, + ERROR_SOURCEFILEMISSING, + ERROR_CANCELED +}; + +}; + +#endif // ERRORCODES_H diff --git a/libs/bsatk/include/bsatk/filehash.h b/libs/bsatk/include/bsatk/filehash.h new file mode 100644 index 0000000..f5bfd6b --- /dev/null +++ b/libs/bsatk/include/bsatk/filehash.h @@ -0,0 +1,30 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 FILEHASH_H +#define FILEHASH_H + +#include + +#include "bsatypes.h" + +BSAHash calculateBSAHash(const std::string& fileName); + +#endif // FILEHASH_H diff --git a/libs/bsatk/src/CMakeLists.txt b/libs/bsatk/src/CMakeLists.txt new file mode 100644 index 0000000..99a0f26 --- /dev/null +++ b/libs/bsatk/src/CMakeLists.txt @@ -0,0 +1,83 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Boost REQUIRED COMPONENTS thread) +find_package(ZLIB REQUIRED) +find_package(mo2-dds-header CONFIG REQUIRED) + +# lz4: try CONFIG first, fall back to pkg-config +find_package(lz4 CONFIG QUIET) +if (NOT lz4_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LZ4 REQUIRED IMPORTED_TARGET liblz4) +endif() +# lz4 1.10+ exports LZ4::lz4, older versions export lz4::lz4 +if(TARGET LZ4::lz4 AND NOT TARGET lz4::lz4) + add_library(lz4::lz4 ALIAS LZ4::lz4) +endif() + +add_library(bsatk STATIC) +target_link_libraries(bsatk + PUBLIC mo2::dds-header + PRIVATE ZLIB::ZLIB Boost::thread +) + +# Link lz4 based on how it was found +if (lz4_FOUND) + target_link_libraries(bsatk PRIVATE lz4::lz4) +else() + target_link_libraries(bsatk PRIVATE PkgConfig::LZ4) +endif() + +target_sources(bsatk + PRIVATE + bsaarchive.cpp + bsaexception.cpp + bsafile.cpp + bsafolder.cpp + bsatypes.cpp + filehash.cpp + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_LIST_DIR}/../include + FILES + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/bsaarchive.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/bsaexception.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/bsafile.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/bsafolder.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/bsatk.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/bsatypes.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/errorcodes.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/filehash.h + +) + +set_target_properties(bsatk PROPERTIES CXX_STANDARD 20) + +# for building +target_include_directories(bsatk PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk) + +if (MSVC) + target_compile_options(bsatk + PRIVATE + "/MP" + "/W4" + "/external:anglebrackets" + "/external:W0" + ) + target_link_options(bsatk + PRIVATE + $<$:/LTCG /INCREMENTAL:NO /OPT:REF /OPT:ICF> + ) + + set_target_properties(bsatk PROPERTIES VS_STARTUP_PROJECT bsatk) +endif() + +add_library(mo2::bsatk ALIAS bsatk) + +# install +install(TARGETS bsatk EXPORT bsatkTargets FILE_SET HEADERS) +install(EXPORT bsatkTargets + FILE mo2-bsatk-targets.cmake + NAMESPACE mo2:: + DESTINATION lib/cmake/mo2-bsatk +) diff --git a/libs/bsatk/src/bsaarchive.cpp b/libs/bsatk/src/bsaarchive.cpp new file mode 100644 index 0000000..e1655c1 --- /dev/null +++ b/libs/bsatk/src/bsaarchive.cpp @@ -0,0 +1,1237 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 "bsaarchive.h" +#include "bsaexception.h" +#include "bsafile.h" +#include "bsafolder.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif + +using std::fstream; + +using namespace boost::posix_time; + +namespace BSA +{ + +Archive::Archive() + : m_RootFolder(new Folder), m_ArchiveFlags(FLAG_HASDIRNAMES | FLAG_HASFILENAMES), + m_Type(TYPE_SKYRIM) +{} + +Archive::~Archive() +{ + if (m_File.is_open()) { + m_File.close(); + } + std::vector folders; + m_RootFolder->collectFolders(folders); + cleanFolder(m_RootFolder); + m_RootFolder.reset(); +} + +ArchiveType Archive::typeFromID(BSAULong typeID) +{ + switch (typeID) { + case 0x100: + return TYPE_MORROWIND; + case 0x67: + return TYPE_OBLIVION; + case 0x68: + return TYPE_FALLOUT3; + case 0x69: + return TYPE_SKYRIMSE; + case 0x01: + return TYPE_FALLOUT4; + case 0x02: + return TYPE_STARFIELD; + case 0x03: + return TYPE_STARFIELD_LZ4_TEXTURE; + case 0x07: + return TYPE_FALLOUT4NG_7; + case 0x08: + return TYPE_FALLOUT4NG_8; + default: + throw data_invalid_exception(makeString("invalid type %d", typeID)); + } +} + +BSAULong Archive::typeToID(ArchiveType type) +{ + switch (type) { + case TYPE_MORROWIND: + return 0x100; + case TYPE_OBLIVION: + return 0x67; + case TYPE_FALLOUT3: + return 0x68; + case TYPE_SKYRIMSE: + return 0x69; + case TYPE_FALLOUT4: + return 0x01; + case TYPE_STARFIELD: + return 0x02; + case TYPE_STARFIELD_LZ4_TEXTURE: + return 0x03; + case TYPE_FALLOUT4NG_7: + return 0x07; + case TYPE_FALLOUT4NG_8: + return 0x08; + default: + throw data_invalid_exception(makeString("invalid type %d", type)); + } +} + +Archive::Header Archive::readHeader(std::fstream& infile) +{ + Header result; + + result.fileIdentifier = readType(infile); + if (result.fileIdentifier != 0x00415342 && result.fileIdentifier != 0x58445442 && + result.fileIdentifier != 0x00000100) { + throw data_invalid_exception(makeString("not a bsa or ba2 file")); + } + + if (result.fileIdentifier != 0x00000100) { + ArchiveType type = typeFromID(readType(infile)); + if (type == TYPE_FALLOUT4 || type == TYPE_STARFIELD || + type == TYPE_STARFIELD_LZ4_TEXTURE || type == TYPE_FALLOUT4NG_7 || + type == TYPE_FALLOUT4NG_8) { + result.type = type; + infile.read(result.archType, 4); + result.archType[4] = '\0'; + result.fileCount = readType(infile); + result.nameTableOffset = readType(infile); + result.archiveFlags = FLAG_HASDIRNAMES | FLAG_HASFILENAMES; + } else { + result.type = type; + result.offset = readType(infile); + result.archiveFlags = readType(infile); + result.folderCount = readType(infile); + result.fileCount = readType(infile); + result.folderNameLength = readType(infile); + result.fileNameLength = readType(infile); + result.fileFlags = readType(infile); + } + } else { + result.type = TYPE_MORROWIND; + result.offset = readType(infile); + result.fileCount = readType(infile); + result.archiveFlags = FLAG_HASDIRNAMES | FLAG_HASFILENAMES; + } + + return result; +} + +EErrorCode Archive::read(const char* fileName, bool testHashes) +{ + m_File.open(fileName, fstream::in | fstream::binary); + if (!m_File.is_open()) { + return ERROR_FILENOTFOUND; + } + m_File.exceptions(std::ios_base::badbit); + try { + Header header; + try { + header = readHeader(m_File); + } catch (const data_invalid_exception& e) { + throw data_invalid_exception(makeString("%s (filename: %s)", e.what(), fileName)); + } + m_ArchiveFlags = header.archiveFlags; + m_Type = header.type; + if (m_Type == TYPE_FALLOUT4 || m_Type == TYPE_STARFIELD || + m_Type == TYPE_STARFIELD_LZ4_TEXTURE || m_Type == TYPE_FALLOUT4NG_7 || + m_Type == TYPE_FALLOUT4NG_8) { + + m_File.seekg(header.nameTableOffset); + + std::vector fileNames; + for (unsigned int i = 0; i < header.fileCount; ++i) { + BSAUShort length = readType(m_File); + + char* file = new char[length + 1]; + m_File.read(file, length); + file[length] = '\0'; + + fileNames.push_back(file); + delete[] file; + } + std::streamoff offset; + switch (m_Type) { + case TYPE_STARFIELD: + offset = 32; + break; + case TYPE_STARFIELD_LZ4_TEXTURE: + offset = 36; + break; + default: + offset = 24; + } + if (strcmp(header.archType, "GNRL") == 0) { + m_File.seekg(offset, std::ios::beg); + for (unsigned int i = 0; i < header.fileCount; ++i) { + BSAUInt nameHash = readType(m_File); + char* extension = new char[4]; + m_File.read(extension, 4); + BSAUInt dirHash = readType(m_File); + m_File.seekg(4, std::ios::cur); + BSAHash offset = readType(m_File); + BSAUInt packedSize = readType(m_File); + BSAUInt unpackedSize = readType(m_File); + m_File.seekg(4, std::ios::cur); + std::vector dummy; + Folder::Ptr newDir = m_RootFolder->addFolderFromFile( + fileNames[i], packedSize, offset, unpackedSize, {}, dummy); + delete[] extension; + } + } else if (strcmp(header.archType, "DX10") == 0) { + m_File.seekg(offset, std::ios::beg); + for (unsigned int i = 0; i < header.fileCount; ++i) { + FO4TextureHeader texHeader; + texHeader.nameHash = readType(m_File); + m_File.read(texHeader.extension, 4); + texHeader.dirHash = readType(m_File); + texHeader.unknown1 = readType(m_File); + texHeader.chunkNumber = readType(m_File); + texHeader.chunkHeaderSize = readType(m_File); + texHeader.height = readType(m_File); + texHeader.width = readType(m_File); + texHeader.mipCount = readType(m_File); + texHeader.format = static_cast(readType(m_File)); + texHeader.isCubemap = readType(m_File); + texHeader.unknown2 = readType(m_File); + std::vector chunks; + for (unsigned int j = 0; j < texHeader.chunkNumber; ++j) { + FO4TextureChunk chunk; + chunk.offset = readType(m_File); + chunk.packedSize = readType(m_File); + chunk.unpackedSize = readType(m_File); + chunk.startMip = readType(m_File); + chunk.endMip = readType(m_File); + chunk.unknown = readType(m_File); + chunks.push_back(chunk); + } + Folder::Ptr newDir = m_RootFolder->addFolderFromFile( + fileNames[i], chunks[0].packedSize, chunks[0].offset, + chunks[0].unpackedSize, texHeader, chunks); + } + } + + return ERROR_NONE; + } else if (m_Type == TYPE_MORROWIND) { + BSAUInt dataOffset = 12 + header.offset + header.fileCount * 8; + + std::vector fileSizeOffset(header.fileCount); + m_File.read((char*)fileSizeOffset.data(), + header.fileCount * sizeof(MorrowindFileOffset)); + std::vector fileNameOffset(header.fileCount); + m_File.read((char*)fileNameOffset.data(), header.fileCount * sizeof(BSAUInt)); + BSAUInt last = header.offset - 12 * header.fileCount; + for (uint32_t i = 0; i < header.fileCount; ++i) { + uint32_t index = 0; + if (i + 1 == header.fileCount) + index = last; + else + index = fileNameOffset[i + 1] - fileNameOffset[i]; + char* filePath = new char[index + 1]; + m_File.read(filePath, index); + filePath[index] = '\0'; + + std::vector dummy; + Folder::Ptr newDir = m_RootFolder->addFolderFromFile( + filePath, fileSizeOffset[i].size, dataOffset + fileSizeOffset[i].offset, 0, + {}, dummy); + delete[] filePath; + } + return ERROR_NONE; + } else { + // flat list of folders as they were stored in the archive + std::vector folders; + + for (unsigned long i = 0; i < header.folderCount; ++i) { + folders.push_back(m_RootFolder->addFolder(m_File, header.fileNameLength, + header.offset, header.type)); + } + + m_File.seekg(header.offset); + + bool hashesValid = true; + for (std::vector::iterator iter = folders.begin(); + iter != folders.end(); ++iter) { + if (!(*iter)->resolveFileNames(m_File, testHashes)) { + hashesValid = false; + } + } + return hashesValid ? ERROR_NONE : ERROR_INVALIDHASHES; + } + } catch (std::ios_base::failure&) { + return ERROR_INVALIDDATA; + } +} + +void Archive::close() +{ + m_File.close(); +} + +BSAULong Archive::countFiles() const +{ + return m_RootFolder->countFiles(); +} + +std::vector Archive::collectFolderNames() const +{ + std::vector result; + m_RootFolder->collectFolderNames(result); + return result; +} + +std::vector Archive::collectFileNames() const +{ + std::vector result; + m_RootFolder->collectFileNames(result); + return result; +} + +BSAULong Archive::countCharacters(const std::vector& list) const +{ + size_t sum = 0; + for (std::vector::const_iterator iter = list.begin(); iter != list.end(); + ++iter) { + sum += iter->length() + 1; + } + return static_cast(sum); +} + +#ifndef WIN32 +#define _stricmp strcasecmp +#endif // WIN32 + +static bool endsWith(const std::string& fileName, const char* extension) +{ + size_t endLength = strlen(extension); + if (fileName.length() < endLength) { + return false; + } + return _stricmp(&fileName[fileName.length() - endLength], extension) == 0; +} + +BSAULong Archive::determineFileFlags(const std::vector& fileList) const +{ + BSAULong result = 0; + + bool hasNIF = false; + bool hasDDS = false; + bool hasXML = false; + bool hasWAV = false; + bool hasMP3 = false; + bool hasTXT = false; + bool hasSPT = false; + bool hasTEX = false; + bool hasCTL = false; + + for (std::vector::const_iterator iter = fileList.begin(); + iter != fileList.end(); ++iter) { + if (!hasNIF && endsWith(*iter, ".nif")) { + hasNIF = true; + result |= 1 << 0; + } else if (!hasDDS && endsWith(*iter, ".dds")) { + hasDDS = true; + result |= 1 << 1; + } else if (!hasXML && endsWith(*iter, ".xml")) { + hasXML = true; + result |= 1 << 2; + } else if (!hasWAV && endsWith(*iter, ".wav")) { + hasWAV = true; + result |= 1 << 3; + } else if (!hasMP3 && endsWith(*iter, ".mp3")) { + hasMP3 = true; + result |= 1 << 4; + } else if (!hasTXT && endsWith(*iter, ".txt")) { + hasTXT = true; + result |= 1 << 5; + } else if (!hasSPT && endsWith(*iter, ".spt")) { + hasSPT = true; + result |= 1 << 6; + } else if (!hasTEX && endsWith(*iter, ".tex")) { + hasTEX = true; + result |= 1 << 7; + } else if (!hasCTL && endsWith(*iter, ".ctl")) { + hasCTL = true; + result |= 1 << 8; + } + } + return result; +} + +void Archive::writeHeader(std::fstream& outfile, BSAULong fileFlags, + BSAULong numFolders, BSAULong folderNamesLength, + BSAULong fileNamesLength) +{ + outfile.write("BSA\0", 4); + writeType(outfile, typeToID(m_Type)); + writeType(outfile, 0x24); // header size is static + writeType(outfile, m_ArchiveFlags); + writeType(outfile, numFolders); + writeType(outfile, countFiles()); + writeType(outfile, folderNamesLength); + writeType(outfile, fileNamesLength); + writeType(outfile, fileFlags); +} + +EErrorCode Archive::write(const char* fileName) +{ + std::fstream outfile; + outfile.open(fileName, fstream::out | fstream::binary); + if (!outfile.is_open()) { + return ERROR_ACCESSFAILED; + } + outfile.exceptions(std::ios_base::badbit); + + std::vector folders; + m_RootFolder->collectFolders(folders); + + std::vector folderNames; + std::vector fileNames; + BSAULong folderNamesLength = 0; + BSAULong fileNamesLength = 0; + for (std::vector::const_iterator folderIter = folders.begin(); + folderIter != folders.end(); ++folderIter) { + std::string fullPath = (*folderIter)->getFullPath(); + folderNames.push_back(fullPath); + folderNamesLength += static_cast(fullPath.length()); + for (std::vector::const_iterator fileIter = + (*folderIter)->m_Files.begin(); + fileIter != (*folderIter)->m_Files.end(); ++fileIter) { + fileNames.push_back((*fileIter)->m_Name); + fileNamesLength += static_cast((*fileIter)->m_Name.length()); + } + } + + try { + writeHeader(outfile, determineFileFlags(fileNames), + static_cast(folderNames.size()), folderNamesLength, + fileNamesLength); +#pragma message("folders (and files?) need to be sorted by hash!") + // dummy-pass: before we can store the actual folder data + + // prepare folder and file headers +#pragma message("it's unnecessary to write actual data, placeholders are sufficient") + for (std::vector::const_iterator folderIter = folders.begin(); + folderIter != folders.end(); ++folderIter) { + (*folderIter)->writeHeader(outfile); + } + + for (std::vector::const_iterator folderIter = folders.begin(); + folderIter != folders.end(); ++folderIter) { + (*folderIter)->writeData(outfile, fileNamesLength); + } + + // write file names + for (std::vector::const_iterator folderIter = fileNames.begin(); + folderIter != fileNames.end(); ++folderIter) { + writeZString(outfile, *folderIter); + } + + // write file data + for (std::vector::iterator folderIter = folders.begin(); + folderIter != folders.end(); ++folderIter) { + (*folderIter)->writeFileData(m_File, outfile); + } + + outfile.seekp(0x24, fstream::beg); + + // re-write folder and file structure, this time with the correct + // offsets + for (std::vector::const_iterator folderIter = folders.begin(); + folderIter != folders.end(); ++folderIter) { + (*folderIter)->writeHeader(outfile); + } + + for (std::vector::const_iterator folderIter = folders.begin(); + folderIter != folders.end(); ++folderIter) { + (*folderIter)->writeData(outfile, fileNamesLength); + } + + outfile.close(); + return ERROR_NONE; + } catch (std::ios_base::failure&) { + outfile.close(); + return ERROR_INVALIDDATA; + } +} + +DirectX::DDS_HEADER Archive::getDDSHeader(File::Ptr file, + DirectX::DDS_HEADER_DXT10& DX10Header, + bool& isDX10) const +{ + DirectX::DDS_HEADER DDSHeaderData = {}; + DDSHeaderData.size = sizeof(DDSHeaderData); + DDSHeaderData.flags = + DDS_HEADER_FLAGS_TEXTURE | DDS_HEADER_FLAGS_LINEARSIZE | DDS_HEADER_FLAGS_MIPMAP; + DDSHeaderData.height = file->m_TextureHeader.height; + DDSHeaderData.width = file->m_TextureHeader.width; + DDSHeaderData.mipMapCount = file->m_TextureHeader.mipCount; + DDSHeaderData.ddspf.size = sizeof(DirectX::DDS_PIXELFORMAT); + DDSHeaderData.caps = DDS_SURFACE_FLAGS_TEXTURE | DDS_SURFACE_FLAGS_MIPMAP; + + if (file->m_TextureHeader.isCubemap) + DDSHeaderData.caps2 = DDS_CUBEMAP_ALLFACES; + + bool supported = true; + + switch (file->m_TextureHeader.format) { + case DXGI_FORMAT_BC1_UNORM: + case DXGI_FORMAT_BC1_UNORM_SRGB: + DDSHeaderData.ddspf = DirectX::DDSPF_DXT1; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height / 2; + break; + + case DXGI_FORMAT_BC2_UNORM: + case DXGI_FORMAT_BC2_UNORM_SRGB: + DDSHeaderData.ddspf = DirectX::DDSPF_DXT3; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + break; + + case DXGI_FORMAT_BC3_UNORM: + case DXGI_FORMAT_BC3_UNORM_SRGB: + DDSHeaderData.ddspf = DirectX::DDSPF_DXT5; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + break; + + case DXGI_FORMAT_BC4_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_BC4_UNORM; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + break; + + case DXGI_FORMAT_BC4_SNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_BC4_SNORM; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + break; + + case DXGI_FORMAT_BC5_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_BC5_UNORM; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + break; + + case DXGI_FORMAT_BC5_SNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_BC5_SNORM; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + break; + + case DXGI_FORMAT_BC7_UNORM: + case DXGI_FORMAT_BC7_UNORM_SRGB: + DDSHeaderData.ddspf = DirectX::DDSPF_DX10; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + + isDX10 = true; + DX10Header.dxgiFormat = file->m_TextureHeader.format; + break; + + case DXGI_FORMAT_R8G8B8A8_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_A8B8G8R8; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height * 4; // 32bpp + break; + + case DXGI_FORMAT_B8G8R8A8_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_A8R8G8B8; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height * 4; // 32bpp + break; + + case DXGI_FORMAT_B8G8R8X8_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_X8R8G8B8; + break; + + case DXGI_FORMAT_R8_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_L8; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; // 8bpp + break; + + case DXGI_FORMAT_R16_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_L16; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height * 2; // 16bpp + break; + + case DXGI_FORMAT_R8G8_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_A8L8; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height * 2; // 16bpp + break; + + default: + return {}; + break; + } + + return DDSHeaderData; +} + +void Archive::getDX10Header(DirectX::DDS_HEADER_DXT10& DX10Header, File::Ptr file, + DirectX::DDS_HEADER DDSHeader) const +{ + DX10Header.resourceDimension = DirectX::DDS_DIMENSION_TEXTURE2D; + DX10Header.miscFlag = 0; + DX10Header.arraySize = 1; + DX10Header.miscFlags2 = 0; +} + +static const unsigned long CHUNK_SIZE = 128 * 1024; + +EErrorCode Archive::extractDirect(File::Ptr file, std::ofstream& outFile) const +{ + EErrorCode result = ERROR_NONE; + if (file->m_FileSize == 0) { + // don't try to read empty file + return result; + } + + m_File.clear(); + m_File.seekg(static_cast(file->m_DataOffset), std::ios::beg); + + if (m_Type == TYPE_FALLOUT4 || m_Type == TYPE_STARFIELD || + m_Type == TYPE_STARFIELD_LZ4_TEXTURE || m_Type == TYPE_FALLOUT4NG_7 || + m_Type == TYPE_FALLOUT4NG_8) { + if (!file->m_TextureChunks.size()) { + BSAULong size = file->m_UncompressedFileSize; + std::unique_ptr buffer(new char[size]); + m_File.read(buffer.get(), size); + outFile.write(buffer.get(), size); + } else { + bool isDX10 = false; + DirectX::DDS_HEADER_DXT10 DX10HeaderData = {}; + DirectX::DDS_HEADER DDSHeaderData = getDDSHeader(file, DX10HeaderData, isDX10); + + outFile.write("DDS ", 4); + char* DDSHeader = new char[sizeof(DDSHeaderData)]; + memcpy(DDSHeader, &DDSHeaderData, sizeof(DDSHeaderData)); + outFile.write(DDSHeader, sizeof(DDSHeaderData)); + delete[] DDSHeader; + + if (isDX10) { + getDX10Header(DX10HeaderData, file, DDSHeaderData); + char* DX10Header = new char[sizeof(DX10HeaderData)]; + memcpy(DX10Header, &DX10HeaderData, sizeof(DX10HeaderData)); + outFile.write(DX10Header, sizeof(DX10HeaderData)); + delete[] DX10Header; + } + + for (BSAUInt i = 0; i < file->m_TextureChunks.size(); ++i) { + BSAULong length = file->m_TextureChunks[i].unpackedSize; + std::unique_ptr chunk(new char[length]); + m_File.read(chunk.get(), length); + outFile.write(chunk.get(), length); + } + } + } else { + BSAULong size = file->m_FileSize; + if (namePrefixed()) { + std::string fullName = readBString(m_File); + if (size <= fullName.length()) { +#pragma message("report error!") + return result; + } + size -= fullName.length() + 1; + } + std::unique_ptr buffer(new unsigned char[size]); + m_File.read(reinterpret_cast(buffer.get()), size); + if (result == ERROR_NONE) + outFile.write(reinterpret_cast(buffer.get()), size); + } + std::unique_ptr inBuffer(new char[CHUNK_SIZE]); + + try { + unsigned long sizeLeft = file->m_FileSize; + + while (sizeLeft > 0) { + int chunkSize = (std::min)(sizeLeft, CHUNK_SIZE); + m_File.read(inBuffer.get(), chunkSize); + outFile.write(inBuffer.get(), chunkSize); + sizeLeft -= chunkSize; + } + } catch (const std::exception&) { + result = ERROR_INVALIDDATA; + } + return result; +} + +std::shared_ptr Archive::decompress(unsigned char* inBuffer, + BSAULong inSize, + EErrorCode& result, + BSAULong& outSize) +{ + if (outSize == 0) { + memcpy(&outSize, inBuffer, sizeof(BSAULong)); + inBuffer += sizeof(BSAULong); + inSize -= sizeof(BSAULong); + } + + if ((inSize == 0) || (outSize == 0)) { + return std::shared_ptr(); + } + + std::shared_ptr outBuffer(new unsigned char[outSize]); + + z_stream stream; + try { + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = inSize; + stream.next_in = static_cast(inBuffer); + int zlibRet = inflateInit2(&stream, 15 + 32); + if (zlibRet != Z_OK) { + result = ERROR_ZLIBINITFAILED; + return std::shared_ptr(); + } + + do { + stream.avail_out = outSize; + stream.next_out = reinterpret_cast(outBuffer.get()); + zlibRet = inflate(&stream, Z_NO_FLUSH); + if ((zlibRet != Z_OK) && (zlibRet != Z_STREAM_END) && (zlibRet != Z_BUF_ERROR)) { +#pragma message("pass result code to caller") + throw std::runtime_error("invalid data"); + } + } while (stream.avail_out == 0); + inflateEnd(&stream); + return outBuffer; + } catch (const std::exception&) { + result = ERROR_INVALIDDATA; + inflateEnd(&stream); + return std::shared_ptr(); + } +} + +EErrorCode Archive::extractCompressed(File::Ptr file, std::ofstream& outFile) const +{ + EErrorCode result = ERROR_NONE; + if (file->m_FileSize == 0) { + // don't try to read empty file + return result; + } + + m_File.clear(); + m_File.seekg(static_cast(file->m_DataOffset), std::ios::beg); + + if (m_Type == TYPE_FALLOUT4 || m_Type == TYPE_STARFIELD || + m_Type == TYPE_STARFIELD_LZ4_TEXTURE || m_Type == TYPE_FALLOUT4NG_7 || + m_Type == TYPE_FALLOUT4NG_8) { + if (!file->m_TextureChunks.size()) { + BSAULong inSize = file->m_FileSize; + std::unique_ptr inBuffer(new unsigned char[inSize]); + m_File.read(reinterpret_cast(inBuffer.get()), inSize); + BSAULong length = file->m_UncompressedFileSize; + std::shared_ptr buffer = + decompress(inBuffer.get(), inSize, result, length); + if (result == ERROR_NONE) { + outFile.write(reinterpret_cast(buffer.get()), length); + } + } else { + bool isDX10 = false; + DirectX::DDS_HEADER_DXT10 DX10HeaderData = {}; + DirectX::DDS_HEADER DDSHeaderData = getDDSHeader(file, DX10HeaderData, isDX10); + + outFile.write("DDS ", 4); + char* DDSHeader = new char[sizeof(DDSHeaderData)]; + memcpy(DDSHeader, &DDSHeaderData, sizeof(DDSHeaderData)); + outFile.write(DDSHeader, sizeof(DDSHeaderData)); + delete[] DDSHeader; + + if (isDX10) { + getDX10Header(DX10HeaderData, file, DDSHeaderData); + char* DX10Header = new char[sizeof(DX10HeaderData)]; + memcpy(DX10Header, &DX10HeaderData, sizeof(DX10HeaderData)); + outFile.write(DX10Header, sizeof(DX10HeaderData)); + delete[] DX10Header; + } + + for (BSAUInt i = 0; i < file->m_TextureChunks.size(); ++i) { + BSAULong length = file->m_TextureChunks[i].unpackedSize; + if (file->m_TextureChunks[i].packedSize > 0) { + unsigned char* chunk = new unsigned char[file->m_TextureChunks[i].packedSize]; + m_File.read(reinterpret_cast(chunk), + file->m_TextureChunks[i].packedSize); + if (m_Type == TYPE_STARFIELD_LZ4_TEXTURE) { + char* unpackedChunk = new char[length]; + LZ4_decompress_safe(reinterpret_cast(chunk), unpackedChunk, + file->m_TextureChunks[i].packedSize, length); + outFile.write(unpackedChunk, length); + delete[] unpackedChunk; + } else { + std::shared_ptr unpackedChunk = + decompress(chunk, file->m_TextureChunks[i].packedSize, result, length); + if (result == ERROR_NONE) { + outFile.write(reinterpret_cast(unpackedChunk.get()), length); + } + } + delete[] chunk; + } else { + char* chunk = new char[length]; + m_File.read(chunk, length); + outFile.write(chunk, length); + delete[] chunk; + } + } + } + } else if (m_Type == TYPE_SKYRIMSE) { + BSAULong inSize = file->m_FileSize; + if (namePrefixed()) { + std::string fullName = readBString(m_File); + if (inSize <= fullName.length()) { +#pragma message("report error!") + return result; + } + inSize -= fullName.length() + 1; + } + BSAULong outSize = readType(m_File); + inSize -= sizeof(BSAULong); + std::unique_ptr inBuffer(new unsigned char[inSize]); + m_File.read(reinterpret_cast(inBuffer.get()), inSize); + + LZ4F_decompressionContext_t dcContext = nullptr; + LZ4F_decompressOptions_t options = {}; + LZ4F_createDecompressionContext(&dcContext, LZ4F_VERSION); + size_t lzOutSize = outSize; + size_t lzInSize = inSize; + + std::unique_ptr outBuffer(new unsigned char[outSize]); + LZ4F_decompress(dcContext, outBuffer.get(), &lzOutSize, inBuffer.get(), &lzInSize, + &options); + + outFile.write(reinterpret_cast(outBuffer.get()), outSize); + } else { + BSAULong inSize = file->m_FileSize; + if (namePrefixed()) { + std::string fullName = readBString(m_File); + if (inSize <= fullName.length()) { +#pragma message("report error!") + return result; + } + inSize -= fullName.length() + 1; + } + std::unique_ptr inBuffer(new unsigned char[inSize]); + m_File.read(reinterpret_cast(inBuffer.get()), inSize); + BSAULong length = 0UL; + std::shared_ptr buffer = + decompress(inBuffer.get(), inSize, result, length); + if (result == ERROR_NONE) { + outFile.write(reinterpret_cast(buffer.get()), length); + } + } + + return result; +} + +EErrorCode Archive::extract(File::Ptr file, const char* outputDirectory) const +{ + std::string fileName = makeString("%s/%s", outputDirectory, file->getName().c_str()); + std::ofstream outputFile(fileName.c_str(), + fstream::out | fstream::binary | fstream::trunc); + if (!outputFile.is_open()) { + return ERROR_ACCESSFAILED; + } + + EErrorCode result = ERROR_NONE; + if (compressed(file)) { + result = extractCompressed(file, outputFile); + } else { + result = extractDirect(file, outputFile); + } + outputFile.close(); + return result; +} + +void Archive::readFiles(std::queue& queue, boost::mutex& mutex, + boost::interprocess::interprocess_semaphore& bufferCount, + boost::interprocess::interprocess_semaphore& queueFree, + std::vector::iterator begin, + std::vector::iterator end) +{ + for (; begin != end && !boost::this_thread::interruption_requested(); ++begin) { + queueFree.wait(); + + FileInfo fileInfo; + fileInfo.file = *begin; + size_t size = static_cast(fileInfo.file->m_FileSize); + + m_File.seekg(fileInfo.file->m_DataOffset); + if (m_Type != TYPE_FALLOUT4 && m_Type != TYPE_STARFIELD && + m_Type != TYPE_STARFIELD_LZ4_TEXTURE && m_Type != TYPE_FALLOUT4NG_7 && + m_Type != TYPE_FALLOUT4NG_8) { + if (namePrefixed()) { + std::string fullName = readBString(m_File); + if (size <= fullName.length()) { +#pragma message("report error!") + continue; + } + size -= fullName.length() + 1; + } + if (m_Type == TYPE_SKYRIMSE && compressed(fileInfo.file)) { + fileInfo.file->m_UncompressedFileSize = readType(m_File); + size -= 4; + } + if (!fileInfo.file->m_TextureChunks.size()) { + fileInfo.data = + std::make_pair(std::shared_ptr(new unsigned char[size]), + static_cast(size)); + m_File.read(reinterpret_cast(fileInfo.data.first.get()), size); + } + } else { + if (!fileInfo.file->m_TextureChunks.size()) { + if (size == 0) + size = fileInfo.file->m_UncompressedFileSize; + fileInfo.data = + std::make_pair(std::shared_ptr(new unsigned char[size]), + static_cast(size)); + m_File.read(reinterpret_cast(fileInfo.data.first.get()), size); + } else { + fileInfo.file->m_UncompressedFileSize = 0L; + BSAULong totalSize = 0U; + for (BSAUInt i = 0; i < fileInfo.file->m_TextureChunks.size(); ++i) { + totalSize += fileInfo.file->m_TextureChunks[i].unpackedSize; + } + char* chunkData = new char[totalSize]; + BSAULong currentPos = 0U; + for (BSAUInt i = 0; i < fileInfo.file->m_TextureChunks.size(); ++i) { + BSAULong length = fileInfo.file->m_TextureChunks[i].unpackedSize; + if (fileInfo.file->m_TextureChunks[i].packedSize > 0) { + char* chunk = new char[fileInfo.file->m_TextureChunks[i].packedSize]; + m_File.read(chunk, fileInfo.file->m_TextureChunks[i].packedSize); + if (m_Type == TYPE_FALLOUT4 || m_Type == TYPE_STARFIELD || + m_Type == TYPE_FALLOUT4NG_7 || m_Type == TYPE_FALLOUT4NG_8) { + EErrorCode result = ERROR_NONE; + try { + std::shared_ptr unpackedChunk = decompress( + reinterpret_cast(chunk), + fileInfo.file->m_TextureChunks[i].packedSize, result, length); + memcpy(chunkData + currentPos, + reinterpret_cast(unpackedChunk.get()), length); + unpackedChunk.reset(); + } catch (const std::exception&) { +#pragma message("report error!") + continue; + } + } else { + char* unpackedChunk = new char[length]; + LZ4_decompress_safe(chunk, unpackedChunk, + fileInfo.file->m_TextureChunks[i].packedSize, length); + memcpy(chunkData + currentPos, unpackedChunk, length); + delete[] unpackedChunk; + } + delete[] chunk; + fileInfo.file->m_UncompressedFileSize += length; + } else { + char* chunk = new char[length]; + m_File.read(chunk, length); + memcpy(chunkData + currentPos, chunk, length); + delete[] chunk; + } + currentPos += length; + } + fileInfo.file->m_FileSize = 0; + fileInfo.data = std::make_pair(std::shared_ptr( + reinterpret_cast(chunkData)), + static_cast(totalSize)); + } + } + + { + boost::interprocess::scoped_lock lock(mutex); + queue.push(fileInfo); + } + bufferCount.post(); + } +} + +inline bool fileExists(const std::string& name) +{ + struct stat buffer; + return stat(name.c_str(), &buffer) != -1; +} + +void Archive::extractFiles(const std::string& targetDirectory, + std::queue& queue, boost::mutex& mutex, + boost::interprocess::interprocess_semaphore& bufferCount, + boost::interprocess::interprocess_semaphore& queueFree, + int totalFiles, bool overwrite, int& filesDone) +{ + for (int i = 0; i < totalFiles; ++i) { + bufferCount.wait(); + if (boost::this_thread::interruption_requested()) { + break; + } + + FileInfo fileInfo; + + { + boost::interprocess::scoped_lock lock(mutex); + fileInfo = queue.front(); + ++filesDone; + queue.pop(); + } + queueFree.post(); + + DataBuffer dataBuffer = fileInfo.data; + + std::string fileName = makeString("%s\\%s", targetDirectory.c_str(), + fileInfo.file->getFilePath().c_str()); + if (!overwrite && fileExists(fileName)) { + continue; + } + + std::ofstream outputFile(fileName.c_str(), + fstream::out | fstream::binary | fstream::trunc); + + if (!outputFile.is_open()) { +#pragma message("report error!") + continue; + // return ERROR_ACCESSFAILED; + } + + if (m_Type != TYPE_FALLOUT4 && m_Type != TYPE_STARFIELD && + m_Type != TYPE_STARFIELD_LZ4_TEXTURE && m_Type != TYPE_FALLOUT4NG_7 && + m_Type != TYPE_FALLOUT4NG_8) { + // BSA extraction + if (compressed(fileInfo.file)) { + // Decompress data + if (m_Type != TYPE_SKYRIMSE) { + // Oblivion - Skyrim LE use gzip compression + EErrorCode result = ERROR_NONE; + try { + BSAULong length = 0UL; + std::shared_ptr buffer = + decompress(dataBuffer.first.get(), dataBuffer.second, result, length); + if (buffer.get() != nullptr) { + outputFile.write(reinterpret_cast(buffer.get()), length); + buffer.reset(); + } + } catch (const std::exception&) { +#pragma message("report error!") + dataBuffer.first.reset(); + fileInfo.data.first.reset(); + continue; + } + } else { + // Skyrim SE uses LZ4 Frame compression + if (!fileInfo.file->m_TextureChunks.size()) { + char* outBuffer = new char[fileInfo.file->m_UncompressedFileSize]; + + LZ4F_decompressionContext_t dcContext = nullptr; + LZ4F_decompressOptions_t options = {}; + LZ4F_createDecompressionContext(&dcContext, LZ4F_VERSION); + size_t outSize = fileInfo.file->m_UncompressedFileSize; + size_t inSize = dataBuffer.second; + + LZ4F_decompress(dcContext, outBuffer, &outSize, dataBuffer.first.get(), + &inSize, &options); + outputFile.write(outBuffer, fileInfo.file->m_UncompressedFileSize); + LZ4F_freeDecompressionContext(dcContext); + delete[] outBuffer; + } + } + } else { + // No compression - just write the data. + outputFile.write(reinterpret_cast(dataBuffer.first.get()), + dataBuffer.second); + } + } else { + // BA2 format + if (fileInfo.file->m_TextureChunks.size()) { + // Texture stream format - requires building the header data for the DDS file + bool isDX10 = false; + DirectX::DDS_HEADER_DXT10 DX10HeaderData = {}; + DirectX::DDS_HEADER DDSHeaderData = + getDDSHeader(fileInfo.file, DX10HeaderData, isDX10); + + outputFile.write("DDS ", 4); + char* DDSHeader = new char[sizeof(DDSHeaderData)]; + memcpy(DDSHeader, &DDSHeaderData, sizeof(DDSHeaderData)); + outputFile.write(DDSHeader, sizeof(DDSHeaderData)); + delete[] DDSHeader; + + if (isDX10) { + // This format requires DX10 header info + getDX10Header(DX10HeaderData, fileInfo.file, DDSHeaderData); + + char* DX10Header = new char[sizeof(DX10HeaderData)]; + memcpy(DX10Header, &DX10HeaderData, sizeof(DX10HeaderData)); + outputFile.write(DX10Header, sizeof(DX10HeaderData)); + delete[] DX10Header; + } + } + + EErrorCode result = ERROR_NONE; + try { + if (fileInfo.file->m_FileSize > 0 && !fileInfo.file->m_TextureChunks.size()) { + BSAULong length = fileInfo.file->m_UncompressedFileSize; + std::shared_ptr buffer = + decompress(dataBuffer.first.get(), dataBuffer.second, result, length); + if (buffer.get() != nullptr) { + outputFile.write(reinterpret_cast(buffer.get()), length); + buffer.reset(); + } + } else { + outputFile.write(reinterpret_cast(dataBuffer.first.get()), + dataBuffer.second); + } + } catch (const std::exception&) { +#pragma message("report error!") + dataBuffer.first.reset(); + fileInfo.data.first.reset(); + continue; + } + } + dataBuffer.first.reset(); + fileInfo.data.first.reset(); + } +} + +void Archive::createFolders(const std::string& targetDirectory, Folder::Ptr folder) +{ + for (std::vector::iterator iter = folder->m_SubFolders.begin(); + iter != folder->m_SubFolders.end(); ++iter) { + std::string subDirName = targetDirectory + "/" + (*iter)->getName(); +#ifdef _WIN32 + ::CreateDirectoryA(subDirName.c_str(), nullptr); +#else + mkdir(subDirName.c_str(), 0755); +#endif + createFolders(subDirName, *iter); + } +} + +EErrorCode Archive::extractAll( + const char* outputDirectory, + const std::function& progress, + bool overwrite) +{ +#pragma message("report errors") + createFolders(outputDirectory, m_RootFolder); + + std::vector fileList; + m_RootFolder->collectFiles(fileList); + std::sort(fileList.begin(), fileList.end(), ByOffset); + m_File.seekg((*(fileList.begin()))->m_DataOffset); + + std::queue buffers; + boost::mutex queueMutex; + int filesDone = 0; + boost::interprocess::interprocess_semaphore bufferCount(0); + boost::interprocess::interprocess_semaphore queueFree(100); + + boost::thread readerThread(boost::bind(&Archive::readFiles, this, boost::ref(buffers), + boost::ref(queueMutex), + boost::ref(bufferCount), boost::ref(queueFree), + fileList.begin(), fileList.end())); + + boost::thread extractThread(boost::bind( + &Archive::extractFiles, this, outputDirectory, boost::ref(buffers), + boost::ref(queueMutex), boost::ref(bufferCount), boost::ref(queueFree), + static_cast(fileList.size()), overwrite, boost::ref(filesDone))); + + bool readerDone = false; + bool extractDone = false; + bool canceled = false; + while (!readerDone || !extractDone) { + if (!readerDone) { + readerDone = readerThread.timed_join(boost::posix_time::millisec(100)); + } + if (readerDone) { + extractDone = extractThread.timed_join(boost::posix_time::millisec(100)); + // don't cancel extractor before reader is done or else reader may be stuck trying + // to write to a queue + if (canceled) { + // ensure the extract thread wakes up. + extractThread.interrupt(); + bufferCount.post(); + } + } + size_t index = (std::min)(static_cast(filesDone), fileList.size() - 1); + if (!progress((filesDone * 100) / static_cast(fileList.size()), + fileList[index]->getName()) && + !canceled) { + readerThread.interrupt(); + canceled = true; // don't interrupt repeatedly + } + } + + return ERROR_NONE; +} + +bool Archive::compressed(const File::Ptr& file) const +{ + if (m_Type != TYPE_FALLOUT4 && m_Type != TYPE_FALLOUT4NG_7 && + m_Type != TYPE_FALLOUT4NG_8) + return file->compressToggled() ^ defaultCompressed(); + return (file->m_FileSize > 0); +} + +File::Ptr Archive::createFile(const std::string& name, const std::string& sourceName, + bool compressed) +{ + return File::Ptr( + new File(name, sourceName, nullptr, defaultCompressed() != compressed)); +} + +void Archive::cleanFolder(Folder::Ptr folder) +{ + std::vector folders; + folder->collectFolders(folders); + for (Folder::Ptr subFolder : folders) { + cleanFolder(subFolder); + } + std::vector files; + folder->collectFiles(files); + for (File::Ptr file : files) { + file.reset(); + } + folder.reset(); +} + +} // namespace BSA diff --git a/libs/bsatk/src/bsaexception.cpp b/libs/bsatk/src/bsaexception.cpp new file mode 100644 index 0000000..9422848 --- /dev/null +++ b/libs/bsatk/src/bsaexception.cpp @@ -0,0 +1,39 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 "bsaexception.h" + +#include +#include + +#pragma warning(disable : 4996) + +std::string makeString(const char* format, ...) +{ + va_list argList; + va_start(argList, format); + char buffer[1024]; + vsnprintf(buffer, 1024, format, argList); + return std::string(buffer); +} + +data_invalid_exception::data_invalid_exception(const std::string& message) + : m_Message(message) +{} diff --git a/libs/bsatk/src/bsafile.cpp b/libs/bsatk/src/bsafile.cpp new file mode 100644 index 0000000..db4e4e6 --- /dev/null +++ b/libs/bsatk/src/bsafile.cpp @@ -0,0 +1,154 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 "bsafile.h" + +#include +#include +#include +#include +#include + +#include "bsaexception.h" +#include "bsafolder.h" +#include "filehash.h" + +using std::fstream; +using std::ifstream; +using std::ofstream; + +namespace BSA +{ + +bool ByOffset(const File::Ptr& LHS, const File::Ptr& RHS) +{ + return LHS->getDataOffset() < RHS->getDataOffset(); +} + +static const unsigned long CHUNK_SIZE = 128 * 1024; + +File::File(std::fstream& file, Folder* folder) + : m_Folder(folder), m_New(false), m_FileSize(0), m_UncompressedFileSize(0), + m_ToggleCompressedWrite(false), m_DataOffsetWrite(0) +{ + m_NameHash = readType(file); + m_FileSize = readType(file); + m_DataOffset = readType(file); + m_ToggleCompressed = m_FileSize & COMPRESSMASK; + m_FileSize = m_FileSize & SIZEMASK; +} + +File::File(const std::string& name, Folder* folder, BSAULong fileSize, + BSAHash dataOffset, BSAULong uncompressedFileSize, FO4TextureHeader header, + std::vector& texChunks) + : m_Folder(folder), m_New(false), m_Name(name), m_FileSize(fileSize), + m_UncompressedFileSize(uncompressedFileSize), m_DataOffset(dataOffset), + m_TextureHeader(header), m_ToggleCompressedWrite(false), + m_TextureChunks(texChunks), m_DataOffsetWrite(0) +{ + m_NameHash = calculateBSAHash(name); + m_ToggleCompressed = false; + if (m_FileSize > 0 && m_UncompressedFileSize > 0) + m_ToggleCompressed = true; +} + +File::File(const std::string& name, const std::string& sourceFile, Folder* folder, + bool toggleCompressed) + : m_Folder(folder), m_New(true), m_Name(name), m_FileSize(0), + m_UncompressedFileSize(0), m_DataOffset(0), m_ToggleCompressed(toggleCompressed), + m_SourceFile(sourceFile), m_ToggleCompressedWrite(toggleCompressed), + m_DataOffsetWrite(0) +{ + m_NameHash = calculateBSAHash(name); +} + +std::string File::getFilePath() const +{ + return m_Folder->getFullPath() + "/" + m_Name; +} + +void File::writeHeader(fstream& file) const +{ + writeType(file, m_NameHash); + BSAULong size = m_FileSize; + if (m_ToggleCompressed) { + size |= (1 << 30); + } + writeType(file, size); + writeType(file, m_DataOffsetWrite); +} + +EErrorCode File::writeData(fstream& sourceArchive, fstream& targetArchive) const +{ + m_DataOffsetWrite = static_cast(targetArchive.tellp()); + EErrorCode result = ERROR_NONE; + + std::unique_ptr inBuffer(new char[CHUNK_SIZE]); + + if (m_SourceFile.length() == 0) { + // copy from source archive +#pragma message("we may have to compress/decompress!") + sourceArchive.seekg(m_DataOffset, fstream::beg); + + try { + unsigned long sizeLeft = m_FileSize; + while (sizeLeft > 0) { + int chunkSize = (std::min)(sizeLeft, CHUNK_SIZE); + sourceArchive.read(inBuffer.get(), chunkSize); + targetArchive.write(inBuffer.get(), chunkSize); + sizeLeft -= chunkSize; + } + } catch (const std::exception&) { + result = ERROR_INVALIDDATA; + } + } else { + // copy from file on disc + fstream sourceFile; + sourceFile.open(m_SourceFile.c_str()); + if (!sourceFile.is_open()) { + return ERROR_SOURCEFILEMISSING; + } + sourceFile.seekg(0, fstream::end); + m_FileSize = static_cast(sourceFile.tellg()); + unsigned long sizeLeft = m_FileSize; + sourceFile.seekg(0, fstream::beg); + while (sizeLeft > 0) { + int chunkSize = (std::min)(sizeLeft, CHUNK_SIZE); + sourceFile.read(inBuffer.get(), chunkSize); + targetArchive.write(inBuffer.get(), chunkSize); + sizeLeft -= chunkSize; + } + } + return result; +} + +void File::readFileName(fstream& file, bool testHashes) +{ + m_Name = readZString(file); + if (testHashes) { + if (calculateBSAHash(m_Name) != m_NameHash) { + throw data_invalid_exception( + makeString("invalid name hash for \"%s\" (%lx vs %lx)", m_Name.c_str(), + calculateBSAHash(m_Name), m_NameHash)); + } + } +} + +} // namespace BSA diff --git a/libs/bsatk/src/bsafolder.cpp b/libs/bsatk/src/bsafolder.cpp new file mode 100644 index 0000000..21e2611 --- /dev/null +++ b/libs/bsatk/src/bsafolder.cpp @@ -0,0 +1,342 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 +#include + +#include "bsaarchive.h" +#include "bsaexception.h" +#include "bsafile.h" +#include "bsafolder.h" + +using std::fstream; + +namespace BSA +{ + +Folder::Folder() : m_Parent(nullptr), m_Name() +{ + m_NameHash = calculateBSAHash(m_Name); + m_FileCount = 0; + m_Offset = ULONG_MAX; +} + +Folder::Ptr Folder::readFolder(std::fstream& file, BSAUInt fileNamesLength, + BSAUInt& endPos) +{ + Folder::Ptr result(new Folder()); + result->m_NameHash = readType(file); + result->m_FileCount = readType(file); + result->m_Offset = readType(file); + std::streamoff pos = file.tellg(); + + file.seekg(result->m_Offset - fileNamesLength, fstream::beg); + + result->m_Name = readBString(file); + + for (unsigned long i = 0UL; i < result->m_FileCount; ++i) { + result->m_Files.push_back(File::Ptr(new File(file, result.get()))); + } + + if (static_cast(file.tellg()) > endPos) { + endPos = static_cast(file.tellg()); + } + + file.seekg(pos); + + return result; +} + +Folder::Ptr Folder::readFolderSE(std::fstream& file, BSAUInt fileNamesLength, + BSAUInt& endPos) +{ + Folder::Ptr result(new Folder()); + result->m_NameHash = readType(file); + result->m_FileCount = readType(file); + readType(file); + result->m_Offset = readType(file); + std::streamoff pos = file.tellg(); + + file.seekg(result->m_Offset - fileNamesLength, fstream::beg); + + result->m_Name = readBString(file); + + for (unsigned long i = 0UL; i < result->m_FileCount; ++i) { + result->m_Files.push_back(File::Ptr(new File(file, result.get()))); + } + + if (static_cast(file.tellg()) > endPos) { + endPos = static_cast(file.tellg()); + } + + file.seekg(pos); + + return result; +} + +void Folder::writeHeader(std::fstream& file) const +{ + writeType(file, m_NameHash); + writeType(file, static_cast(m_Files.size())); + writeType(file, m_OffsetWrite); +} + +void Folder::writeData(std::fstream& file, BSAULong fileNamesLength) const +{ + m_OffsetWrite = static_cast(file.tellp()) + fileNamesLength; + writeBString(file, getFullPath()); + for (std::vector::const_iterator iter = m_Files.begin(); + iter != m_Files.end(); ++iter) { + (*iter)->writeHeader(file); + } +} + +EErrorCode Folder::writeFileData(std::fstream& sourceFile, + std::fstream& targetFile) const +{ + for (std::vector::const_iterator iter = m_Files.begin(); + iter != m_Files.end(); ++iter) { + EErrorCode error = (*iter)->writeData(sourceFile, targetFile); + if (error != ERROR_NONE) { + return error; + } + } + return ERROR_NONE; +} + +std::string Folder::getFullPath() const +{ + if (m_Parent != nullptr) { + std::string temp = m_Parent->getFullPath(); + if (temp.length() != 0) { + return temp.append("\\").append(m_Name); + } else { + return m_Name; + } + } else { + // root folder shouldn't have a name + return std::string(); + } +} + +void Folder::addFolderInt(Folder::Ptr folder) +{ + std::filesystem::path path(folder->m_Name); + auto it = path.begin(); + std::string firstStr = it->string(); + std::filesystem::path remaining; + for (++it; it != path.end(); ++it) { + remaining /= *it; + } + + if (m_SubFoldersByName.contains(firstStr)) { + // remove the matched part of the path and recurse + folder->m_Name = remaining.string(); + m_SubFoldersByName.at(firstStr)->addFolderInt(folder); + return; + } + + // no subfolder matches, create one + if (remaining.empty()) { + // no more path components, add the new folder right here + folder->m_Parent = this; + m_SubFolders.push_back(folder); + m_SubFoldersByName[firstStr] = folder; + } else { + // add dummy folder for the next path component + Folder::Ptr dummy(new Folder); + dummy->m_Parent = this; + dummy->m_Name = firstStr; + folder->m_Name = remaining.string(); + dummy->addFolderInt(folder); + m_SubFolders.push_back(dummy); + m_SubFoldersByName[firstStr] = dummy; + } +} + +Folder::Ptr Folder::addOrFindFolderInt(Folder* folder) +{ + std::filesystem::path path(folder->m_Name); + auto it = path.begin(); + std::string firstStr = it->string(); + std::filesystem::path remaining; + for (++it; it != path.end(); ++it) { + remaining /= *it; + } + + if (m_SubFoldersByName.contains(firstStr)) { + if (!remaining.empty()) { + // remove the matched part of the path and recurse + folder->m_Name = remaining.string(); + return m_SubFoldersByName.at(firstStr)->addOrFindFolderInt(folder); + } else { + return m_SubFoldersByName.at(firstStr); + } + } + + // no subfolder matches, create one + if (remaining.empty()) { + // no more path components, add the new folder right here + folder->m_Parent = this; + m_SubFolders.push_back(Folder::Ptr(folder)); + m_SubFoldersByName[firstStr] = m_SubFolders.back(); + return m_SubFolders.back(); + } else { + // add dummy folder for the next path component + Folder::Ptr dummy(new Folder); + dummy->m_Parent = this; + dummy->m_Name = firstStr; + folder->m_Name = remaining.string(); + Folder::Ptr result = dummy->addOrFindFolderInt(folder); + m_SubFolders.push_back(dummy); + m_SubFoldersByName[firstStr] = dummy; + return result; + } +} + +Folder::Ptr Folder::addFolder(std::fstream& file, BSAUInt fileNamesLength, + BSAUInt& endPos, ArchiveType type) +{ + Folder::Ptr temp; + if (type == ArchiveType::TYPE_SKYRIMSE) + temp = readFolderSE(file, fileNamesLength, endPos); + else + temp = readFolder(file, fileNamesLength, endPos); + addFolderInt(temp); + + return temp; +} + +Folder::Ptr Folder::addFolderFromFile(std::string filePath, BSAUInt size, + BSAHash offset, BSAUInt uncompressedSize, + FO4TextureHeader header, + std::vector& texChunks) +{ + std::filesystem::path file(filePath); + + Folder* tempFolder = new Folder(); + + tempFolder->m_NameHash = calculateBSAHash(filePath); + tempFolder->m_Name = file.parent_path().string(); + Folder::Ptr result = addOrFindFolderInt(tempFolder); + + if (result.get() != tempFolder) { + delete tempFolder; + } + + std::string fileName = file.filename().string(); + + result->m_FileCount++; + result->m_Files.push_back(File::Ptr(new File(fileName, result.get(), size, offset, + uncompressedSize, header, texChunks))); + + return result; +} + +bool Folder::resolveFileNames(std::fstream& file, bool testHashes) +{ + bool hashesValid = true; + for (std::vector::iterator iter = m_Files.begin(); iter != m_Files.end(); + ++iter) { + try { + (*iter)->readFileName(file, testHashes); + } catch (const std::exception&) { + hashesValid = false; + } + } + return hashesValid; +} + +const Folder::Ptr Folder::getSubFolder(unsigned int index) const +{ + return m_SubFolders.at(index); +} + +unsigned int Folder::countFiles() const +{ + unsigned int result = 0; + for (std::vector::const_iterator iter = m_SubFolders.begin(); + iter != m_SubFolders.end(); ++iter) { + result += (*iter)->countFiles(); + } + return result + static_cast(m_Files.size()); +} + +const File::Ptr Folder::getFile(unsigned int index) const +{ + return m_Files.at(index); +} + +Folder::Ptr Folder::addFolder(const std::string& folderName) +{ + Folder::Ptr newFolder(new Folder); + newFolder->m_Name = folderName; + newFolder->m_Parent = this; + m_SubFolders.push_back(newFolder); + return newFolder; +} + +void Folder::collectFolders(std::vector& folderList) const +{ + for (std::vector::const_iterator iter = m_SubFolders.begin(); + iter != m_SubFolders.end(); ++iter) { + if ((*iter)->m_Files.size() != 0) { + folderList.push_back(*iter); + } + (*iter)->collectFolders(folderList); + } +} + +void Folder::collectFiles(std::vector& fileList) const +{ + for (std::vector::const_iterator fileIter = m_Files.begin(); + fileIter != m_Files.end(); ++fileIter) { + fileList.push_back(*fileIter); + } + for (std::vector::const_iterator folderIter = m_SubFolders.begin(); + folderIter != m_SubFolders.end(); ++folderIter) { + (*folderIter)->collectFiles(fileList); + } +} + +void Folder::collectFileNames(std::vector& nameList) const +{ + for (std::vector::const_iterator iter = m_Files.begin(); + iter != m_Files.end(); ++iter) { + nameList.push_back((*iter)->getName()); + } + for (std::vector::const_iterator iter = m_SubFolders.begin(); + iter != m_SubFolders.end(); ++iter) { + (*iter)->collectFileNames(nameList); + } +} + +void Folder::collectFolderNames(std::vector& nameList) const +{ + if (m_Files.size() != 0) { + nameList.push_back(getFullPath()); + } + for (std::vector::const_iterator iter = m_SubFolders.begin(); + iter != m_SubFolders.end(); ++iter) { + (*iter)->collectFolderNames(nameList); + } +} + +} // namespace BSA diff --git a/libs/bsatk/src/bsatypes.cpp b/libs/bsatk/src/bsatypes.cpp new file mode 100644 index 0000000..eb33af3 --- /dev/null +++ b/libs/bsatk/src/bsatypes.cpp @@ -0,0 +1,70 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 "bsatypes.h" + +#include +#include +#include +#include +#include + +using std::fstream; + +/*#ifndef MAX_PATH +#define MAX_PATH PATH_MAX +#endif // MAX_PATH +*/ + +std::string readBString(fstream& file) +{ + unsigned char length = readType(file); + char buffer[256]; + if (length > 0) { + if (!file.read(buffer, length)) { + throw data_invalid_exception("can't read from bsa"); + } + } + buffer[length] = '\0'; + return std::string(buffer); +} + +void writeBString(fstream& file, const std::string& string) +{ + unsigned int length = + std::min(static_cast(string.length()), 255); + writeType(file, length + 1); + file.write(string.c_str(), length + 1); +} + +std::string readZString(fstream& file) +{ + char buffer[FILENAME_MAX]; + memset(buffer, '\0', FILENAME_MAX); + if (!file.getline(buffer, FILENAME_MAX, '\0')) { + throw data_invalid_exception("can't read from bsa"); + } + return std::string(buffer); +} + +void writeZString(fstream& file, const std::string& string) +{ + file.write(string.c_str(), string.length() + 1); +} diff --git a/libs/bsatk/src/filehash.cpp b/libs/bsatk/src/filehash.cpp new file mode 100644 index 0000000..717a02c --- /dev/null +++ b/libs/bsatk/src/filehash.cpp @@ -0,0 +1,100 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 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 "filehash.h" + +#include +#include +#include +#include + +#ifndef MAX_PATH +#define MAX_PATH PATH_MAX +#endif // MAX_PATH + +static unsigned long genHashInt(const unsigned char* pos, const unsigned char* end) +{ + unsigned long hash = 0; + for (; pos < end; ++pos) { + hash *= 0x1003f; + hash += *pos; + } + return hash; +} + +/** + * @brief calculateBSAHash + * @param fileName + * @return + * @note the hash calculated for folders seem to be wrong + */ +BSAHash calculateBSAHash(const std::string& fileName) +{ + char fileNameLower[FILENAME_MAX + 1]; + int i = 0; + for (; i < FILENAME_MAX && fileName[i] != '\0'; ++i) { + fileNameLower[i] = tolower(fileName[i]); + if (fileNameLower[i] == '/') { + fileNameLower[i] = '\\'; + } + } + fileNameLower[i] = '\0'; + + unsigned char* fileNameLowerU = reinterpret_cast(fileNameLower); + + size_t length = strlen(fileNameLower); + + char* ext = strrchr(fileNameLower, '.'); + if (ext == nullptr) { + ext = fileNameLower + length; + } + + size_t extLen = strlen(ext); + length -= extLen; + + unsigned char* extU = reinterpret_cast(ext); + + BSAHash hash1 = 0ULL; + + if (length > 0) { + hash1 = static_cast(fileNameLowerU[length - 1] | + ((length > 2 ? fileNameLowerU[length - 2] : 0) << 8) | + (length << 16) | (fileNameLowerU[0] << 24)); + } + + if (extLen > 0) { + if (strcmp(ext + 1, "kf") == 0) { + hash1 |= 0x80; + } else if (strcmp(ext + 1, "nif") == 0) { + hash1 |= 0x8000; + } else if (strcmp(ext + 1, "dds") == 0) { + hash1 |= 0x8080; + } else if (strcmp(ext + 1, "wav") == 0) { + hash1 |= 0x80000000; + } + + BSAHash hash2 = static_cast(genHashInt(fileNameLowerU + 1, extU - 2)) + + static_cast(genHashInt(extU, extU + strlen(ext))); + + hash1 |= (hash2 & 0xFFFFFFFF) << 32; + } + + return hash1; +} diff --git a/libs/bsatk/vcpkg.json b/libs/bsatk/vcpkg.json new file mode 100644 index 0000000..7a82d05 --- /dev/null +++ b/libs/bsatk/vcpkg.json @@ -0,0 +1,30 @@ +{ + "dependencies": [ + "mo2-dds-header", + "zlib", + "lz4", + "boost-thread", + "boost-interprocess" + ], + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43", + "packages": ["boost*", "boost-*"] + }, + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "228cda39fe9d1eeed789c0ef64fd1235dab3b11e", + "packages": ["mo2-dds-header"] + } + ] + } +} diff --git a/libs/check_fnis/.clang-format b/libs/check_fnis/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/check_fnis/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/check_fnis/.gitattributes b/libs/check_fnis/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/check_fnis/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/check_fnis/.gitignore b/libs/check_fnis/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/check_fnis/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/check_fnis/.pre-commit-config.yaml b/libs/check_fnis/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/check_fnis/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/check_fnis/CMakeLists.txt b/libs/check_fnis/CMakeLists.txt new file mode 100644 index 0000000..f7eb6af --- /dev/null +++ b/libs/check_fnis/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(check_fnis) + +add_subdirectory(src) diff --git a/libs/check_fnis/CMakePresets.json b/libs/check_fnis/CMakePresets.json new file mode 100644 index 0000000..cc009b3 --- /dev/null +++ b/libs/check_fnis/CMakePresets.json @@ -0,0 +1,57 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/check_fnis/src/CMakeLists.txt b/libs/check_fnis/src/CMakeLists.txt new file mode 100644 index 0000000..38451b1 --- /dev/null +++ b/libs/check_fnis/src/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB check_fnis_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(check_fnis SHARED ${check_fnis_SOURCES}) +mo2_configure_plugin(check_fnis NO_SOURCES WARNINGS OFF) +target_link_libraries(check_fnis PRIVATE mo2::uibase) + +mo2_install_plugin(check_fnis) diff --git a/libs/check_fnis/src/check_fnis_en.ts b/libs/check_fnis/src/check_fnis_en.ts new file mode 100644 index 0000000..408428e --- /dev/null +++ b/libs/check_fnis/src/check_fnis_en.ts @@ -0,0 +1,66 @@ + + + + + CheckFNIS + + + FNIS Checker + + + + + Checks if FNIS behaviours need to be updated whenever you start the game. This is only relevant for Skyrim and if FNIS is installed.<br> + + + + + Run FNIS before %1? + + + + + FNIS source data has been changed. You should run GenerateFNIS.exe now. + + + + + Failed to start %1 + + + + + + + Start %1? + + + + + FNIS reported a %1, do you want to run the application anyway? + + + + + + warning + + + + + + critical error + + + + + Failed to determine FNIS exit code, do you want to run the application anyway? + + + + + FNIS reported a %1. Do you want to assume it worked? + + + + diff --git a/libs/check_fnis/src/checkfnis.cpp b/libs/check_fnis/src/checkfnis.cpp new file mode 100644 index 0000000..73f2ac0 --- /dev/null +++ b/libs/check_fnis/src/checkfnis.cpp @@ -0,0 +1,293 @@ +#include "checkfnis.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include // for Qt::CaseInsensitive +#include // for qCritical, qDebug + +#include + +using namespace MOBase; + +CheckFNIS::CheckFNIS() + : m_MOInfo(nullptr), m_Active(false), + m_MatchExpressions(std::vector{ + // MSVC2013 bug. The (std::vector shouldn't be necessary + QRegularExpression("\\\\FNIS_.*_List\\.txt$", + QRegularExpression::PatternOption::CaseInsensitiveOption), + QRegularExpression("\\\\FNIS.*Behavior\\.txt$", + QRegularExpression::PatternOption::CaseInsensitiveOption), + QRegularExpression("\\\\PatchList\\.txt$", + QRegularExpression::PatternOption::CaseInsensitiveOption), + QRegularExpression( + "\\\\skeleton.*\\.hkx$", + QRegularExpression::PatternOption::CaseInsensitiveOption)}), + m_SensitiveMatchExpressions(std::vector{ + QRegularExpression("\\\\animations\\\\.*\\.hkx$", + QRegularExpression::PatternOption::CaseInsensitiveOption)}) +{} + +CheckFNIS::~CheckFNIS() {} + +bool CheckFNIS::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + + if (!moInfo->onAboutToRun([this](const auto& binary) { + return fnisCheck(binary); + })) { + qCritical("failed to connect to about to run event"); + return false; + } + + if (!moInfo->onFinishedRun(std::bind(&CheckFNIS::fnisEndCheck, this, + std::placeholders::_1, std::placeholders::_2))) { + qCritical("failed to connect to finished run event"); + return false; + } + + return true; +} + +QString CheckFNIS::name() const +{ + return "FNIS Checker"; +} + +QString CheckFNIS::localizedName() const +{ + return tr("FNIS Checker"); +} + +QString CheckFNIS::author() const +{ + return "Tannin"; +} + +QString CheckFNIS::description() const +{ + return tr("Checks if FNIS behaviours need to be updated whenever you start the game." + " This is only relevant for Skyrim and if FNIS is installed.
"); +} + +VersionInfo CheckFNIS::version() const +{ + return VersionInfo(1, 0, 1, VersionInfo::RELEASE_FINAL); +} + +std::vector> +CheckFNIS::requirements() const +{ + return {Requirements::gameDependency("Skyrim")}; +} + +QList CheckFNIS::settings() const +{ + QList result; + result.push_back(PluginSetting("sensitive", + "check changes on non-fnis animations. Makes this " + "more reliable but will cause FNIS to be called more " + "often than necessary.", + QVariant(false))); + return result; +} + +bool CheckFNIS::testFileRelevant(const IOrganizer::FileInfo& fileName) const +{ + if (!fileName.archive.isEmpty()) { + return false; + } + + for (auto& expr : m_MatchExpressions) { + auto match = expr.match(fileName.filePath); + if (match.hasMatch()) { + return true; + } + } + + if (m_MOInfo->pluginSetting(name(), "sensitive").toBool()) { + for (auto& expr : m_SensitiveMatchExpressions) { + auto match = expr.match(fileName.filePath); + if (match.hasMatch()) { + return true; + } + } + } + + return false; +} + +void CheckFNIS::findRelevantFilesRecursive(const QString& path, + QMap& fileList) const +{ + // find all relevant files + QList files = m_MOInfo->findFileInfos( + path, std::bind(&CheckFNIS::testFileRelevant, this, std::placeholders::_1)); + foreach (const IOrganizer::FileInfo& fileInfo, files) { + QFile file(fileInfo.filePath); + if (file.open(QIODevice::ReadOnly)) { + QString hash( + QCryptographicHash::hash(file.readAll(), QCryptographicHash::Md5).toHex()); + fileList.insert(fileInfo.filePath, hash); + } else { + qCritical("failed to open %s", qUtf8Printable(fileInfo.filePath)); + } + } + + QStringList subDirectories = m_MOInfo->listDirectories(path); + foreach (const QString& directory, subDirectories) { + findRelevantFilesRecursive(QDir(path).filePath(directory), fileList); + } +} + +QString CheckFNIS::generateIdentifier() const +{ + QMap fileList; + + findRelevantFilesRecursive("meshes\\actors", fileList); + + QStringList flattenedList; + for (auto iter = fileList.begin(); iter != fileList.end(); ++iter) { + flattenedList.append(iter.key() + "=" + iter.value()); + } + + return QCryptographicHash::hash(flattenedList.join(",").toUtf8(), + QCryptographicHash::Md5) + .toHex(); +} + +bool CheckFNIS::appIsFNIS(QString const& application, QString const& fnisApp) +{ + return QString::compare(QDir::fromNativeSeparators(application), + QDir::fromNativeSeparators(fnisApp), + Qt::CaseInsensitive) == 0; +} + +QString CheckFNIS::getFnisPath() const +{ + if (!m_MOInfo->pluginSetting(name(), "enabled").toBool()) { + return ""; + } + + // Check if it's actually installed (...) + QStringList fnisBinaryList = m_MOInfo->findFiles( + "tools/GenerateFNIS_for_Users", [](const QString& fileName) -> bool { + return fileName.endsWith("GenerateFNISforUsers.exe", Qt::CaseInsensitive); + }); + + if (fnisBinaryList.count() == 0) { + // fnis seems not to be installed even though this is enabled + qDebug("fnis not installed"); + return ""; + } + + // As this is looking in the vfs, there can be precisely 0 or 1 instances of FNIS + return fnisBinaryList.at(0); +} + +bool CheckFNIS::fnisCheck(const QString& application) +{ + + QString fnisApp(getFnisPath()); + if (fnisApp.isEmpty() || appIsFNIS(application, fnisApp)) { + return true; + } + + // prevent this check from being called recursively + if (m_Active) { + return true; + } + + m_Active = true; + ON_BLOCK_EXIT([&] { + m_Active = false; + }); + + QString const newHash = generateIdentifier(); + + if (newHash == m_MOInfo->persistent(name(), m_MOInfo->profileName(), "").toString()) { + // Don't need to run fnis as nothing relevant has changed. + return true; + } + + QDialogButtonBox::StandardButton res = QuestionBoxMemory::query( + nullptr, "fnisCheck", QFileInfo(application).fileName(), + tr("Run FNIS before %1?").arg(application), + tr("FNIS source data has been changed. You should run GenerateFNIS.exe now."), + QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel, + QDialogButtonBox::Yes); + + if (res == QDialogButtonBox::Yes) { + HANDLE process = m_MOInfo->startApplication(fnisApp); + bool cont = true; + if (process == INVALID_HANDLE_VALUE) { + reportError(tr("Failed to start %1").arg(fnisApp)); + } else { + DWORD exitCodeU; + if (m_MOInfo->waitForApplication(process, &exitCodeU)) { + int exitCode = static_cast(exitCodeU); + if (exitCode != 0) { + cont = QMessageBox::question( + nullptr, tr("Start %1?").arg(application), + tr("FNIS reported a %1, do you want to run the application " + "anyway?") + .arg(exitCode < 0 ? tr("warning") : tr("critical error")), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) == QMessageBox::Yes; + } + } else { + cont = QMessageBox::question(nullptr, tr("Start %1?").arg(application), + tr("Failed to determine FNIS exit code, do you " + "want to run the application " + "anyway?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) == QMessageBox::Yes; + } + if (cont) { + m_MOInfo->setPersistent(name(), m_MOInfo->profileName(), newHash); + } + } + return cont; + } else if (res == QDialogButtonBox::No) { + return true; + } else { + // Don't run the app if they pressed cancel + return false; + } +} + +void CheckFNIS::fnisEndCheck(const QString& application, unsigned int code) +{ + if (appIsFNIS(application, getFnisPath())) { + bool update = true; + int exitCode = static_cast(code); + if (exitCode != 0) { + update = + QMessageBox::question( + nullptr, tr("Start %1?").arg(application), + tr("FNIS reported a %1. Do you want to assume it worked?") + .arg(exitCode < 0 ? tr("warning") : tr("critical error")), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes; + } + if (update) { + m_MOInfo->setPersistent(name(), m_MOInfo->profileName(), generateIdentifier()); + } + } +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(checkfnis, CheckFNIS) +#endif diff --git a/libs/check_fnis/src/checkfnis.h b/libs/check_fnis/src/checkfnis.h new file mode 100644 index 0000000..8971ab2 --- /dev/null +++ b/libs/check_fnis/src/checkfnis.h @@ -0,0 +1,67 @@ +#ifndef CHECKFNIS_H +#define CHECKFNIS_H + +#include +#include +#include + +#include +#include +#include +#include + +#include // for QT_VERSION, QT_VERSION_CHECK + +#include + +namespace MOBase +{ +struct PluginSetting; +} + +class CheckFNIS : public QObject, public MOBase::IPlugin +{ + + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.CheckFNIS") +#endif + +public: + CheckFNIS(); + ~CheckFNIS(); + +public: // IPlugin + virtual bool init(MOBase::IOrganizer* moInfo) override; + virtual QString name() const override; + virtual QString localizedName() const override; + virtual std::vector> + requirements() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +private: + bool fnisCheck(const QString& application); + void fnisEndCheck(const QString& application, unsigned int code); + + QString getFnisPath() const; + static bool appIsFNIS(QString const& application, const QString& fnisApp); + + bool testFileRelevant(const MOBase::IOrganizer::FileInfo& fileName) const; + void findRelevantFilesRecursive(const QString& path, + QMap& fileList) const; + // generates a map of file-names and hashes. Any change prompts a new fnis run + QString generateIdentifier() const; + +private: + MOBase::IOrganizer* m_MOInfo; + bool m_Active; + + std::vector const m_MatchExpressions; + std::vector const m_SensitiveMatchExpressions; +}; + +#endif // CHECKFNIS_H diff --git a/libs/check_fnis/vcpkg.json b/libs/check_fnis/vcpkg.json new file mode 100644 index 0000000..a639ce6 --- /dev/null +++ b/libs/check_fnis/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673" + } + } +} diff --git a/libs/cmake_common/.pre-commit-config.yaml b/libs/cmake_common/.pre-commit-config.yaml new file mode 100644 index 0000000..e00bc0d --- /dev/null +++ b/libs/cmake_common/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/cmake_common/README.md b/libs/cmake_common/README.md new file mode 100644 index 0000000..c66a5a3 --- /dev/null +++ b/libs/cmake_common/README.md @@ -0,0 +1,109 @@ +# MO2 CMake Common + +This repository contains useful CMake macro and functions that are used to ease creating +CMake configuration for most of MO2 repositories (e.g., +[uibase](https://github.com/ModOrganizer2/modorganizer-uibase), +[plugin_python](https://github.com/ModOrganizer2/modorganizer-plugin_python) or +[ModOrganizer2](https://github.com/ModOrganizer2/modorganizer) itself). + +## Getting Started + +### 1. With VCPKG + +Add [MO2 VCPKG registry](https://github.com/ModOrganizer2/vcpkg-registry) to your +`vcpkg-configuration.json`: + +```json +{ + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "f61a294e765b257926ae9e9d85f96468a0af74e7" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "27d8adbfe9e4ce88a875be3a45fadab69869eb60", + "packages": ["mo2-cmake"] + } + ] +} +``` + +Add `mo2-cmake` to your VCPKG dependencies (`vcpkg.json`) and then import the utilities +with `find_package` in your CMake configuration: + +```cmake +find_package(mo2-cmake CONFIG REQUIRED) +``` + +### 2. Manually + +Clone this repository somewhere and then `include(mo2.cmake)` in your CMake +configuration files. + +## Usage + +Be aware that using these utilities will automatically set some (not too intrusive) +global variable on your project. + +In order to properly use this package, you should set `CMAKE_INSTALL_PREFIX` to a valid +location. +There are two possible way of using this package controlled by the `MO2_INSTALL_IS_BIN` +option: + +- if `MO2_INSTALL_IS_BIN` is `OFF` (default), this assumes a layout with a `bin`, + `lib`, `include` and `pdb` folder under `CMAKE_INSTALL_PREFIX`, +- if `MO2_INSTALL_IS_BIN` is `ON` (default when building standalone), this assumes + that `CMAKE_INSTALL_PREFIX` point directly to the equivalent `bin` folder. + +Importing the utilities will make the following variables available: + +- `MO2_QT_VERSION` - The Qt version used by MO2, as `major.minor.patch`. +- `MO2_QT_MAJOR_VERSION`, `MO2_QT_MINOR_VERSION` and `MO2_QT_PATCH_VERSION` - + Respectively the major, minor and patch version of Qt used by MO2. +- `MO2_PYTHON_VERSION` - The Python version used by MO2 as `major.minor` (patch version + should not be relevant). + +All functions are prefixed by `mo2_` and should not conflict with other existing +functions. + +### Generic Utilities + +- `mo2_set_if_not_defined` - Set a variable to a given value if the variable is not + defined. +- `mo2_add_subdirectories` - +- `mo2_find_python_executable` - Find Python executable. +- `mo2_find_git_hash` - Find the hash of the current git HEAD. +- `mo2_find_qt_executable` - Find a given Qt executable. +- `mo2_set_project_to_run_from_install` - Configure the debug executable for a VS project. +- `mo2_add_filter` - Add a source group filter. +- `mo2_deploy_qt_for_tests` - Deploy Qt DLLs, etc., for tests. +- `mo2_deploy_qt` - Deploy Qt DLLs for ModOrganizer2. +- `mo2_add_lupdate` - Create a target to run Qt `lupdate`. +- `mo2_add_lrelease` - Create a target to Qt `lrelease`. +- `mo2_add_translations` - Add targets to generate translations for the given target. + +### C++ Utilities + +TODO: + +- `mo2_configure_warnings` - Utility function configure warnings for a target. +- `mo2_configure_sources` - Glob and configure sources, including Qt-related files + (.ui, etc.) for a target. +- `mo2_configure_msvc` - Set some MSVC-specific flag for a target. +- (Deprecated) `mo2_configure_target` - Combine the above function + Extra stuff. +- (Deprecated) `mo2_configure_plugin` +- `mo2_configure_tests` - TO BE CHANGED +- `mo2_install_plugin` - Install a plugin. + +### Python Utilities + +- `mo2_python_uifiles` - Add a target to generate `.py` files from `.ui` files. +- `mo2_python_pip_install` - Install Python packages. +- `mo2_python_requirements` - Install plugin requirements from a `plugin-requirements.txt` + file, ready to ship. +- `mo2_configure_python_module` - Configure a Python module plugin. +- (Deprecated) `mo2_configure_python_simple` - Configure a Python single file plugin. +- `mo2_configure_python` - Wrapper for the two above functions. diff --git a/libs/cmake_common/mo2-cmake-config.cmake b/libs/cmake_common/mo2-cmake-config.cmake new file mode 100644 index 0000000..3b4ed19 --- /dev/null +++ b/libs/cmake_common/mo2-cmake-config.cmake @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.22) + +include(${CMAKE_CURRENT_LIST_DIR}/mo2.cmake) diff --git a/libs/cmake_common/mo2.cmake b/libs/cmake_common/mo2.cmake new file mode 100644 index 0000000..7e8c3f7 --- /dev/null +++ b/libs/cmake_common/mo2.cmake @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.22) + +if (DEFINED MO2_DEFINED) + return() +endif() + +option(MO2_INSTALL_IS_BIN + "if set, CMAKE_INSTALL_PREFIX is assumed to point to bin rather than one level below") + +if (MO2_INSTALL_IS_BIN) + set(MO2_INSTALL_BIN ".") +else() + set(MO2_INSTALL_BIN "bin") +endif() + +include(${CMAKE_CURRENT_LIST_DIR}/mo2_utils.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/mo2_versions.cmake) + +# this makes VS install everything when building solution +if(MSVC) + set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +endif() + +# this find Python globally rather than virtual environments, even when one is active +set(Python_FIND_VIRTUALENV STANDARD) + +# this set the imported location of targets for missing configurations - this silents +# many CMP0111 warnings from CMake +set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL MinSizeRel RelWithDebInfo Release None) +set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO RelWithDebInfo Release MinSizeRel None) +set(CMAKE_MAP_IMPORTED_CONFIG_RELEASE Release RelWithDebInfo MinSizeRel None) + +# allow setting folder property on targets for better organization in VS +set_property(GLOBAL PROPERTY USE_FOLDERS ON) + +# put code generated by Qt in a autogen group in VS +set_property(GLOBAL PROPERTY AUTOGEN_SOURCE_GROUP autogen) +set_property(GLOBAL PROPERTY AUTOMOC_SOURCE_GROUP autogen) +set_property(GLOBAL PROPERTY AUTORCC_SOURCE_GROUP autogen) + +# put targets generated by Qt into a autogen folder (this is not the same as the above) +set(QT_TARGETS_FOLDER autogen) + +include(${CMAKE_CURRENT_LIST_DIR}/mo2_cpp.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/mo2_python.cmake) + +# mark as included +set(MO2_DEFINED true) diff --git a/libs/cmake_common/mo2_cpp.cmake b/libs/cmake_common/mo2_cpp.cmake new file mode 100644 index 0000000..1ed4250 --- /dev/null +++ b/libs/cmake_common/mo2_cpp.cmake @@ -0,0 +1,358 @@ +cmake_minimum_required(VERSION 3.21) + +include(CMakeParseArguments) +include(${CMAKE_CURRENT_LIST_DIR}/mo2_utils.cmake) + +#! mo2_configure_warnings : configuration warning for C++ target +# +# \param:WARNINGS level of warnings, possible values are ON/All, OFF, or 1, 2, 3, 4 +# for corresponding /W flags (ON is All) (default ON) +# \param:EXTERNAL enable warnings for external libraries, possible values are +# the same as warnings, but ON is 3 (default 1) +# +function(mo2_configure_warnings TARGET) + cmake_parse_arguments(MO2 "" "WARNINGS;EXTERNAL" "" ${ARGN}) + + mo2_set_if_not_defined(MO2_WARNINGS ON) + mo2_set_if_not_defined(MO2_EXTERNAL 1) + + if (${MO2_WARNINGS} STREQUAL "ON") + set(MO2_WARNINGS "All") + endif() + + if (${MO2_EXTERNAL} STREQUAL "ON") + set(MO2_EXTERNAL "3") + endif() + + if(NOT (${MO2_WARNINGS} STREQUAL "OFF")) + if(MSVC) + string(TOLOWER ${MO2_WARNINGS} MO2_WARNINGS) + target_compile_options(${TARGET} PRIVATE "/W${MO2_WARNINGS}" "/wd4464") + + # external warnings + if (${MO2_EXTERNAL} STREQUAL "OFF") + target_compile_options(${TARGET} + PRIVATE "/external:anglebrackets" "/external:W0") + else() + string(TOLOWER ${MO2_EXTERNAL} MO2_EXTERNAL) + target_compile_options(${TARGET} + PRIVATE "/external:anglebrackets" "/external:W${MO2_EXTERNAL}") + endif() + elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(${TARGET} PRIVATE -Wall -Wextra -Wno-unused-parameter) + endif() + endif() + +endfunction() + +#! mo2_target_sources : add sources to a given target, eventually putting them in +# a folder +# +# \param: FILES list of .ui files to add +# \param: RC_FILES list of .qrc or .rc files to add +# +function(mo2_target_sources TARGET) + cmake_parse_arguments(MO2 "" "FOLDER" "PRIVATE;PUBLIC" ${ARGN}) + + mo2_set_if_not_defined(MO2_PRIVATE "") + mo2_set_if_not_defined(MO2_PUBLIC "") + + set(_sources "") + + if (MO2_PRIVATE) + target_sources(${TARGET} PRIVATE ${MO2_PRIVATE}) + list(APPEND _sources ${MO2_PRIVATE}) + endif() + + if (MO2_PUBLIC) + target_sources(${TARGET} PUBLIC ${MO2_PUBLIC}) + list(APPEND _sources ${MO2_PUBLIC}) + endif() + + if ((DEFINED MO2_FOLDER) AND _sources) + source_group(${MO2_FOLDER} FILES ${_sources}) + endif() + +endfunction() + +#! mo2_default_source_group : configure default source groups for MO2 +# +# \param:NO_SRC if set, the src source_group will not be created, default if false +# +function(mo2_default_source_group) + cmake_parse_arguments(MO2 "SOURCE_TREE" "" "" ${ARGN}) + + # remove the CMake Rules autogenerated folder + source_group("CMake Rules" REGULAR_EXPRESSION "^$") + source_group(ui REGULAR_EXPRESSION ".*\\.ui") + source_group(cmake FILES CMakeLists.txt) + source_group(autogen REGULAR_EXPRESSION ".*\\cmake_pch.*|.*\\.rule") + source_group(resources REGULAR_EXPRESSION ".*\\.qrc|.*\\.rc") + + if (NOT NO_SRC) + source_group(src REGULAR_EXPRESSION ".*\\.(h|cpp)$") + endif() +endfunction() + +#! mo2_configure_sources : configure sources for the given C++ target +# +# \param:SOURCE_TREE if set, a source_group will be created using TREE +# +function(mo2_configure_sources TARGET) + cmake_parse_arguments(MO2 "SOURCE_TREE" "" "" ${ARGN}) + + # find source files + if(DEFINED AUTOGEN_BUILD_DIR) + set(UI_HEADERS_DIR ${AUTOGEN_BUILD_DIR}) + else() + set(UI_HEADERS_DIR ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}_autogen/include_RelWithDebInfo) + endif() + + file(GLOB_RECURSE source_files CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) + file(GLOB_RECURSE header_files CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.h) + file(GLOB_RECURSE qrc_files CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc) + file(GLOB_RECURSE rc_files CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.rc) + file(GLOB_RECURSE ui_files CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.ui) + file(GLOB_RECURSE ui_header_files CONFIGURE_DEPENDS ${UI_HEADERS_DIR}/*.h) + file(GLOB_RECURSE rule_files CONFIGURE_DEPENDS ${CMAKE_BINARY_DIR}/*.rule) + + + + if (${MO2_SOURCE_TREE}) + mo2_default_source_group(NO_SRC) + source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} + PREFIX src FILES ${source_files} ${header_files}) + else() + mo2_default_source_group() + endif() + + target_sources(${TARGET} + PRIVATE + ${source_files} + ${header_files} + ${ui_files} + ${ui_header_files} + ${qrc_files} + ${rc_files} + ${misc_files} + ${qm_files}) + +endfunction() + +#! mo2_configure_msvc : set flags for C++ target with MSVC +# +# \param:PERMISSIVE permissive mode (default OFF) +# \param:BIGOBJ enable bigobj (default OFF) +# \param:CLI enable C++/CLR (default OFF) +# +function(mo2_configure_msvc TARGET) + + if (NOT MSVC) + return() + endif() + + cmake_parse_arguments(MO2 "" "PERMISSIVE;BIGOBJ;CLI" "" ${ARGN}) + + set(CXX_STANDARD 23) + if (${MO2_CLI}) + set(CXX_STANDARD 17) + endif() + set_target_properties(${TARGET} PROPERTIES + CXX_STANDARD ${CXX_STANDARD} CXX_EXTENSIONS OFF) + + if(NOT ${MO2_PERMISSIVE}) + target_compile_options(${TARGET} PRIVATE "/permissive-") + endif() + + if(${MO2_BIGOBJ}) + target_compile_options(${TARGET} PRIVATE "/bigobj") + endif() + + # multi-threaded compilation + target_compile_options(${TARGET} PRIVATE "/MP") + + # VS emits a warning for LTCG, at least for uibase, so maybe not required? + target_link_options(${TARGET} + PRIVATE + $<$: + # enable link-time code generation + /LTCG + + # disable incremental linking + /INCREMENTAL:NO + + # eliminates functions and data that are never referenced + /OPT:REF + + # perform identical COMDAT folding + /OPT:ICF + >) + + if(${MO2_CLI}) + set_target_properties(${TARGET} PROPERTIES COMMON_LANGUAGE_RUNTIME "") + endif() + + get_property(CURRENT_STARTUP_PROJECT + DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT) + + if (NOT CURRENT_STARTUP_PROJECT) + message(STATUS "MO2: Setting startup project to " ${TARGET} ".") + set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${TARGET}) + endif() + +endfunction() + +#! mo2_configure_target : do basic configuration for a MO2 C++ target +# +# this functions does many things: +# - glob relevant files and add them to the target +# - set many compile flags, definitions, etc. +# - add step to create translations (if not turned OFF) +# +# \param:SOURCE_TREE if set, a source_group will be created using TREE +# \param:NO_SOURCES if set, mo2_configure_sources will not be called +# \param:WARNINGS enable all warnings, possible values are ON/All, OFF, or 1, 2, 3, 4 +# for corresponding /W flags (ON is All) (default ON) +# \param:EXTERNAL_WARNINGS enable warnings for external libraries, possible values are +# the same as warnings, but ON is 3 (default 1) +# \param:PERMISSIVE permissive mode (default OFF) +# \param:BIGOBJ enable bigobj (default OFF) +# \param:CLI enable C++/CLR (default OFF) +# \param:TRANSLATIONS generate translations (default ON) +# \param:AUTOMOC automoc (and autouic, autoqrc), (default ON) +# \param:EXTRA_TRANSLATIONS extra translations to include (folder) +# +function(mo2_configure_target TARGET) + cmake_parse_arguments(MO2 "SOURCE_TREE;NO_SOURCES" + "WARNINGS;EXTERNAL_WARNINGS;PERMISSIVE;BIGOBJ;CLI;TRANSLATIONS;AUTOMOC" + "EXTRA_TRANSLATIONS" + ${ARGN}) + + # configure parameters and compiler flags + mo2_set_if_not_defined(MO2_NO_SOURCES OFF) + mo2_set_if_not_defined(MO2_PERMISSIVE OFF) + mo2_set_if_not_defined(MO2_BIGOBJ OFF) + mo2_set_if_not_defined(MO2_CLI OFF) + mo2_set_if_not_defined(MO2_TRANSLATIONS ON) + mo2_set_if_not_defined(MO2_AUTOMOC ON) + mo2_set_if_not_defined(MO2_EXTRA_TRANSLATIONS "") + + mo2_configure_warnings(${TARGET} ${ARGN}) + mo2_configure_msvc(${TARGET} ${ARGN}) + + if (NOT MO2_NO_SOURCES) + mo2_configure_sources(${TARGET} ${ARGN}) + endif() + + if (${MO2_AUTOMOC}) + find_package(Qt6 COMPONENTS Widgets REQUIRED) + set_target_properties(${TARGET} + PROPERTIES AUTOMOC ON AUTOUIC ON AUTORCC ON) + endif() + + if(${MO2_TRANSLATIONS}) + mo2_add_translations(${TARGET} + INSTALL_RELEASE + SOURCES ${CMAKE_CURRENT_SOURCE_DIR} ${MO2_EXTRA_TRANSLATIONS}) + endif() + + mo2_find_git_hash(GIT_COMMIT_HASH) + target_compile_definitions( + ${TARGET} PRIVATE NOMINMAX QT_MESSAGELOGCONTEXT GITID="${GIT_COMMIT_HASH}") + + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/pch.h) + target_precompile_headers(${PROJECT_NAME} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/pch.h) + endif() +endfunction() + +#! mo2_configure_tests : configure a target as a MO2 C++ tests +# +# this function creates a set of tests available in the ${TARGET}_gtests variable +# +# extra arguments are given to mo2_configure_target, TRANSLATIONS and AUTOMOC are +# OFF by default +# +function(mo2_configure_tests TARGET) + cmake_parse_arguments(MO2 "NO_MOCK;NO_MAIN" "" "" ${ARGN}) + mo2_configure_target(${TARGET} TRANSLATIONS OFF AUTOMOC OFF ${MO2_UNPARSED_ARGUMENTS}) + + find_package(GTest REQUIRED) + target_link_libraries(${TARGET} PRIVATE GTest::gtest) + + if (NOT MO2_NO_MOCK) + target_link_libraries(${TARGET} PRIVATE GTest::gmock) + endif() + if (NOT MO2_NO_MAIN) + target_link_libraries(${TARGET} PRIVATE GTest::gtest_main) + endif() + + # gtest_discover_tests would be nice but it requires Qt DLL, uibase, etc., in the + # path, etc., and is not working right now + # + # there is an open CMake issue: https://gitlab.kitware.com/cmake/cmake/-/issues/21453 + # + # gtest_discover_tests(${TARGET} + # WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin + # PROPERTIES + # VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin + # ) + # + + gtest_add_tests( + TARGET ${TARGET} + TEST_LIST ${TARGET}_gtests + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) + set(${TARGET}_gtests ${${TARGET}_gtests} PARENT_SCOPE) + + if(WIN32) + mo2_deploy_qt_for_tests( + TARGET ${TARGET} + BINARIES "$,EXCLUDE,^.*[/\\]Qt[^/\\]*[.]dll>") + + set_tests_properties(${${TARGET}_gtests} + PROPERTIES + ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$,\;>" + ) + endif() +endfunction() + +#! mo2_configure_plugin : configure a target as a MO2 C++ plugin +# +# this function automatically set uibase as a dependency +# +# extra arguments are given to mo2_configure_target +# +function(mo2_configure_plugin TARGET) + mo2_configure_target(${TARGET} ${ARGN}) + if(WIN32) + mo2_set_project_to_run_from_install( + ${TARGET} EXECUTABLE ${CMAKE_INSTALL_PREFIX}/${MO2_INSTALL_BIN}/ModOrganizer.exe) + endif() +endfunction() + +#! mo2_install_plugin : install the given MO2 plugin +# +# for this to work properly, the target must have been configured +# +# \param:FOLDER install the plugin as a folder, instead of a single DLL +# +function(mo2_install_plugin TARGET) + cmake_parse_arguments(MO2 "FOLDER" "" "" ${ARGN}) + + if (${MO2_FOLDER}) + install(TARGETS ${TARGET} RUNTIME DESTINATION ${MO2_INSTALL_BIN}/plugins/$) + else() + install(TARGETS ${TARGET} RUNTIME DESTINATION ${MO2_INSTALL_BIN}/plugins) + endif() + + if (NOT MO2_INSTALL_IS_BIN) + install(TARGETS ${TARGET} ARCHIVE DESTINATION lib) + if(WIN32) + # install PDB if possible + install(FILES $ DESTINATION pdb OPTIONAL) + endif() + endif() + +endfunction() diff --git a/libs/cmake_common/mo2_python.cmake b/libs/cmake_common/mo2_python.cmake new file mode 100644 index 0000000..c8af496 --- /dev/null +++ b/libs/cmake_common/mo2_python.cmake @@ -0,0 +1,356 @@ +cmake_minimum_required(VERSION 3.16) + +include(${CMAKE_CURRENT_LIST_DIR}/mo2_utils.cmake) + +set(MO2_PYLIBS_DIR "${CMAKE_BINARY_DIR}/pylibs" CACHE PATH + "default for path for Python libraries") + +#! mo2_python_pip_install : run "pip install ..." +# +# \param:TARGET target to install Python package for +# \param:DIRECTORY directory to install libraries to, REQUIRED +# \param:PACKAGES packages to install, REQUIRED, can contain version constraints, e.g., +# "PyQt6==6.3.0" +# +function(mo2_python_pip_install TARGET) + cmake_parse_arguments(MO2 + "NO_DEPENDENCIES;PRE_RELEASE;NO_FORCE;USE_CACHE" "DIRECTORY" "PACKAGES;EXTRA_INDEX_URLS" ${ARGN}) + + mo2_set_if_not_defined(MO2_DIRECTORY ${MO2_PYLIBS_DIR}) + mo2_set_if_not_defined(MO2_NO_DEPENDENCIES OFF) + mo2_set_if_not_defined(MO2_PRE_RELEASE OFF) + mo2_set_if_not_defined(MO2_NO_FORCE OFF) + mo2_set_if_not_defined(MO2_USE_CACHE OFF) + mo2_set_if_not_defined(MO2_EXTRA_INDEX_URLS "") + + set(pip_install_arguments "") + + if (MO2_NO_DEPENDENCIES) + list(APPEND pip_install_arguments --no-deps) + endif() + + if (MO2_PRE_RELEASE) + list(APPEND pip_install_arguments --pre) + endif() + + if (NOT MO2_NO_FORCE) + list(APPEND pip_install_arguments --force) + endif() + + if (NOT USE_CACHE) + list(APPEND pip_install_arguments --no-cache-dir) + endif() + + foreach(_extra_index_url ${MO2_EXTRA_INDEX_URLS}) + list(APPEND pip_install_arguments --extra-index-url ${_extra_index_url}) + endforeach() + + mo2_find_python_executable(PYTHON_EXE) + + string(MAKE_C_IDENTIFIER "${MO2_PACKAGES}" PIP_FILE_LOG) + set(pip_log_file "${CMAKE_CURRENT_BINARY_DIR}/${PIP_FILE_LOG}.log") + + add_custom_command( + OUTPUT "${pip_log_file}" + COMMAND ${PYTHON_EXE} + -I + -m pip + install + ${pip_install_arguments} + --upgrade + --disable-pip-version-check + --isolated + --no-cache-dir + --target="${MO2_DIRECTORY}" + --log="${pip_log_file}" + ${MO2_PACKAGES} + ) + + set(pip_target_name "${TARGET}_pip_${PIP_FILE_LOG}") + + add_custom_target(${pip_target_name} ALL DEPENDS "${pip_log_file}") + set_target_properties(${pip_target_name} PROPERTIES FOLDER autogen) + + add_dependencies(${TARGET} ${pip_target_name}) +endfunction() + +#! mo2_python_install_pyqt : install PyQt6 and create a PyQt6 target for it +# +# it is safe to call this function multiple times, PyQt6 will only be installed once +# +function(mo2_python_install_pyqt) + if (TARGET PyQt6) + return() + endif() + + add_custom_target(PyQt6) + set_target_properties(PyQt6 PROPERTIES FOLDER autogen) + mo2_python_pip_install(PyQt6 NO_FORCE + PACKAGES + PyQt${MO2_QT_VERSION_MAJOR}==${MO2_PYQT_VERSION} + sip==${MO2_SIP_VERSION}) +endfunction() + +#! mo2_python_uifiles : create .py files from .ui files for a python target +# +# \param:TARGET target to generate .py files for +# \param:INPLACE if specified, .py files are generated next to the .ui files, useful +# for Python modules, otherwise files are generated in the binary directory +# \param:FILES list of .ui files to generate .py files from +# +function(mo2_python_uifiles TARGET) + cmake_parse_arguments(MO2 "INPLACE" "" "FILES" ${ARGN}) + + if (NOT MO2_FILES) + return() + endif() + + mo2_find_python_executable(PYTHON_EXE) + mo2_python_install_pyqt() + + message(DEBUG "generating .py from ui files: ${MO2_FILES}") + + set(pyui_files "") + foreach (UI_FILE ${MO2_FILES}) + get_filename_component(name "${UI_FILE}" NAME_WLE) + if (${MO2_INPLACE}) + get_filename_component(folder "${UI_FILE}" DIRECTORY) + else() + set(folder "${CMAKE_CURRENT_BINARY_DIR}") + endif() + + set(output "${folder}/${name}.py") + add_custom_command( + OUTPUT "${output}" + COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${CMAKE_BINARY_DIR}/pylibs + ${MO2_PYLIBS_DIR}/bin/pyuic${MO2_QT_VERSION_MAJOR}.exe + -o "${output}" + "${UI_FILE}" + DEPENDS "${UI_FILE}" + ) + + list(APPEND pyui_files "${output}") + endforeach() + + if (${MO2_INPLACE}) + source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} + PREFIX autogen FILES ${pyui_files}) + endif() + + add_custom_target("${TARGET}_uic" DEPENDS ${pyui_files}) + set_target_properties("${TARGET}_uic" PROPERTIES FOLDER autogen) + + add_dependencies(${TARGET} "${TARGET}_uic") + + add_dependencies("${TARGET}_uic" PyQt6) + +endfunction() + +#! mo2_python_requirements : install requirements for a python target +# +# \param:TARGET target to install requirements for +# \param:LIBDIR library to install requirements to +# +function(mo2_python_requirements TARGET) + cmake_parse_arguments(MO2 "" "LIBDIR" "" ${ARGN}) + + mo2_find_python_executable(PYTHON_EXE) + add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/pip.log" + COMMAND ${PYTHON_EXE} + -I + -m pip + install --force --upgrade --disable-pip-version-check + --target="${MO2_LIBDIR}" + --log="${CMAKE_CURRENT_BINARY_DIR}/pip.log" + -r "${PROJECT_SOURCE_DIR}/plugin-requirements.txt" + DEPENDS "${PROJECT_SOURCE_DIR}/plugin-requirements.txt" + ) + add_custom_target("${TARGET}_libs" + ALL DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/pip.log") + set_target_properties("${TARGET}_libs" PROPERTIES FOLDER autogen) + + add_dependencies(${TARGET} "${TARGET}_libs") + + file(MAKE_DIRECTORY "${MO2_LIBDIR}") + + install( + DIRECTORY "${MO2_LIBDIR}" + DESTINATION "${MO2_INSTALL_BIN}/plugins/${TARGET}/" + PATTERN "__pycache__" EXCLUDE + ) + +endfunction() + +#! mo2_configure_python_module : configure a Python plugin module +# +# \param:TARGET target for the Python plugin +# \param:LIBDIR directory to install requirements (if any) to, default is "lib" +# \param:RESDIR directory to install genereated resources (if any) to, default is "res" +# +function(mo2_configure_python_module TARGET) + cmake_parse_arguments(MO2 "" "LIBDIR;RESDIR" "" ${ARGN}) + + mo2_set_if_not_defined(MO2_LIBDIR "lib") + mo2_set_if_not_defined(MO2_RESDIR "res") + + set(res_dir "${PROJECT_SOURCE_DIR}/${MO2_RESDIR}") + set(lib_dir "${PROJECT_SOURCE_DIR}/${MO2_LIBDIR}") + + # py files + file(GLOB_RECURSE py_files CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.py) + + set(all_src_files ${py_files} ${ui_files} ${qrc_files}) + + set(src_files ${all_src_files}) + list(FILTER src_files EXCLUDE REGEX "${lib_dir}[/\\].*") + + set(lib_files ${all_src_files}) + list(FILTER lib_files INCLUDE REGEX "${lib_dir}[/\\].*") + + target_sources(${TARGET} PRIVATE ${src_files}) + source_group(cmake FILES CMakeLists.txt) + source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} + PREFIX src + FILES ${src_files}) + source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} + PREFIX ${MO2_LIBDIR} + FILES ${lib_files}) + + # ui files + file(GLOB_RECURSE ui_files CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.ui) + mo2_python_uifiles(${TARGET} INPLACE FILES ${ui_files}) + + # install requirements if there are any + if(EXISTS "${PROJECT_SOURCE_DIR}/plugin-requirements.txt") + mo2_python_requirements(${TARGET} LIBDIR "${lib_dir}") + target_sources(${TARGET} PRIVATE + "${PROJECT_SOURCE_DIR}/plugin-requirements.txt" + ) + source_group(requirements + FILES "${PROJECT_SOURCE_DIR}/plugin-requirements.txt") + endif() + + set(install_dir "${MO2_INSTALL_BIN}/plugins/${TARGET}") + + # directories that go in bin/plugins/${name} + install( + DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/" + DESTINATION ${install_dir} + FILES_MATCHING PATTERN "*.py" + PATTERN ".git" EXCLUDE + PATTERN ".github" EXCLUDE + PATTERN ".tox" EXCLUDE + PATTERN ".mypy_cache" EXCLUDE + PATTERN "vsbuild" EXCLUDE) + + # copy the resource directory if it exists + if(EXISTS "${res_dir}") + install( + DIRECTORY "${res_dir}" + DESTINATION ${install_dir} + ) + endif() + +endfunction() + +#! mo2_configure_python_simple : configure a Python plugin (simple file) +# +# \param:TARGET target for the Python plugin +# +function(mo2_configure_python_simple TARGET) + + # this copies all the .py files that are directly in src/ into + # ${install_dir}/ + # + # any folder that contains at least one .py file (recursive) is copied in + # bin/plugins/data + # + + # ui files + file(GLOB ui_files CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.ui) + mo2_python_uifiles(${TARGET} FILES ${ui_files}) + + # .py files directly in the directory + file(GLOB py_files CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.py) + + # .json files directly in the directory + file(GLOB json_files CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.json) + + file(GLOB_RECURSE extra_py_files CONFIGURE_DEPENDS + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/**/*.py) + + set(src_files ${py_files} ${ui_files} ${json_files} ${extra_py_files}) + target_sources(${TARGET} PRIVATE ${src_files}) + source_group(cmake FILES CMakeLists.txt) + source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} + PREFIX src + FILES ${src_files}) + source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} + PREFIX data + FILES ${json_files}) + + set(install_dir "${MO2_INSTALL_BIN}/plugins") + + # .py files directly in src/ go to plugins/ + install(FILES ${py_files} DESTINATION ${install_dir}) + + # folders with Python files go into plugins/data + set(extra_py_dirs ${extra_py_files}) + list(TRANSFORM extra_py_dirs REPLACE "[/\\][^/\\]+" "") + list(REMOVE_DUPLICATES extra_py_dirs) + + install(DIRECTORY ${extra_py_dirs} + DESTINATION "${install_dir}/data" + FILES_MATCHING PATTERN "*.py") + + # JSON file go in plugins/data + install(FILES ${json_files} DESTINATION "${install_dir}/data") + + # generated files go in plugins/data + install( + DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/" + DESTINATION "${install_dir}/data" + FILES_MATCHING + PATTERN "*.py" + PATTERN "CMakeFiles" EXCLUDE + PATTERN "x64" EXCLUDE) + +endfunction() + +#! mo2_configure_python : configure a MO2 python target +# +# \param:MODULE indicates if this is a Python module plugin or a file plugin +# \param:TRANSLATIONS ON to generate translations (default), OFF to not generate them +# \param:LIB only for Python module, see mo2_configure_python_module +# \param:RES only for Python module, see mo2_configure_python_module +# +function(mo2_configure_python TARGET) + cmake_parse_arguments(MO2 "MODULE;SIMPLE" "TRANSLATIONS;LIB;RES" "" ${ARGN}) + + mo2_set_if_not_defined(MO2_TRANSLATIONS ON) + + if ((${MO2_MODULE} AND ${MO2_SIMPLE}) OR (NOT(${MO2_MODULE}) AND NOT(${MO2_SIMPLE}))) + message(FATAL_ERROR "mo2_configure_python should be called with either SIMPLE or MODULE") + endif() + + if (${MO2_MODULE}) + mo2_configure_python_module(${TARGET} ${ARGN}) + else() + mo2_configure_python_simple(${TARGET} ${ARGN}) + endif() + + # do this AFTER configure_ to properly handle the the ui files + if(${MO2_TRANSLATIONS}) + mo2_add_translations(${TARGET} SOURCES ${CMAKE_CURRENT_SOURCE_DIR}) + endif() + + file(GLOB_RECURSE py_files CONFIGURE_DEPENDS *.py) + file(GLOB_RECURSE rc_files CONFIGURE_DEPENDS *.rc) + file(GLOB_RECURSE ui_files CONFIGURE_DEPENDS *.ui) + + target_sources(${TARGET} + PRIVATE ${py_files} ${ui_files} ${rc_files} ${qm_files}) + +endfunction() diff --git a/libs/cmake_common/mo2_utils.cmake b/libs/cmake_common/mo2_utils.cmake new file mode 100644 index 0000000..795ad36 --- /dev/null +++ b/libs/cmake_common/mo2_utils.cmake @@ -0,0 +1,464 @@ +# this file contains utility code that is not directly related to MO2 +cmake_minimum_required(VERSION 3.16) + +include(CMakeParseArguments) + +if (DEFINED MO2_UTILS_DEFINED) + return() +endif() + +#! mo2_set_if_not_defined : set a variable to the given value if not defined +# +# \param:NAME name of the variable +# \param:VALUE value of the variable to set (if not defined) +# +function (mo2_set_if_not_defined NAME VALUE) + if (NOT DEFINED ${NAME}) + set(${NAME} ${VALUE} PARENT_SCOPE) + endif() +endfunction() + + +#! mo2_add_subdirectories : add all repositories matching the given list of patterns +# +# \param:FOLDER Folder (layout) to add the subdirectories to +# \param:GLOB List of glob patterns to find repositories +# +function (mo2_add_subdirectories) + cmake_parse_arguments(MO2 "" "FOLDER" "GLOB" ${ARGN}) + + if (NOT DEFINED MO2_FOLDER) + message(FATAL_ERROR "missing FOLDER in add_subdirectories") + endif() + if (NOT DEFINED MO2_GLOB) + message(FATAL_ERROR "missing GLOB in add_subdirectories") + endif() + + file(GLOB directories RELATIVE ${CMAKE_CURRENT_LIST_DIR} LIST_DIRECTORIES TRUE ${MO2_GLOB}) + + set(CMAKE_FOLDER ${MO2_FOLDER}) + foreach(directory ${directories}) + add_subdirectory(${directory}) + endforeach() + unset(CMAKE_FOLDER) + +endfunction() + +#! mo2_find_python_executable : find the full path to the Python executable +# +# \param:VARNAME name of the variable that will contain the path to Python +function(mo2_find_python_executable VARNAME) + if (NOT DEFINED Python_EXECUTABLE) + find_package(Python ${MO2_PYTHON_VERSION} EXACT COMPONENTS Interpreter REQUIRED) + endif() + set(${VARNAME} ${Python_EXECUTABLE} PARENT_SCOPE) +endfunction() + +#! mo2_find_git_hash : find the git hash of HEAD on the current source project +# +# \param:VARNAME variable to store the git hash +function(mo2_find_git_hash VARNAME) + execute_process( + COMMAND git log -1 --format=%h + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + OUTPUT_VARIABLE GIT_COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + set(${VARNAME} ${GIT_COMMIT_HASH} PARENT_SCOPE) +endfunction() + +#! mo2_find_qt_executable : find the path to the executable from Qt +# +function(mo2_find_qt_executable VARNAME EXECUTABLE) + + # retrieve the absolute path to qmake and then use that path to find + # the windeployqt and macdeployqt binaries + get_target_property(_qmake_executable Qt6::qmake IMPORTED_LOCATION) + get_filename_component(_qt_bin_dir "${_qmake_executable}" DIRECTORY) + + # need to use a custom varname per executable to use the cache + find_program(QT_${EXECUTABLE} ${EXECUTABLE} HINTS "${_qt_bin_dir}") + if(WIN32 AND NOT QT_${EXECUTABLE}) + message(FATAL_ERROR "${EXECUTABLE} not found") + endif() + + set(${VARNAME} ${QT_${EXECUTABLE}} PARENT_SCOPE) +endfunction() + +#! mo2_set_project_to_run_from_install : set a target to run from a given executable +# +# this function is only meaningful for VS generator +# +# \param:TARGET name of the target +# \param:EXECUTABLE full path to the executable +# \param:WORKDIR working directory (optional, default is the directory of the executable) +# +function(mo2_set_project_to_run_from_install TARGET) + cmake_parse_arguments(MO2 "" "EXECUTABLE;WORKDIR" "" ${ARGN}) + + # extract directory + if (NOT DEFINED MO2_WORKDIR) + get_filename_component(MO2_WORKDIR ${MO2_EXECUTABLE} DIRECTORY) + endif() + + set_target_properties(${TARGET} PROPERTIES + VS_DEBUGGER_WORKING_DIRECTORY "${MO2_WORKDIR}" + VS_DEBUGGER_COMMAND "${MO2_EXECUTABLE}") +endfunction() + +#! mo2_required_variable : check that a variable is defined, fails otherwise +# +# this function checks that a variable with the given NAME is defined, if it's not +# it fails with a FATAL_ERROR, otherwise it caches the variable +# +# \param:NAME name of the variable +# \param:TYPE type of the variable (optional) +# \param:DESC description of the variable (optional) +# +macro(mo2_required_variable) + cmake_parse_arguments(REQ_VAR "" "NAME;TYPE;DESC" "" ${ARGN}) + + if(NOT DEFINED REQ_VAR_DESC) + set(REQ_VAR_DESC "${name}") + endif() + + if(NOT DEFINED ${REQ_VAR_NAME}) + message(FATAL_ERROR "${REQ_VAR_NAME} is not defined") + endif() + + set(${REQ_VAR_NAME} ${${REQ_VAR_NAME}} CACHE ${REQ_VAR_TYPE} "${REQ_VAR_DESC}") +endmacro() + +#! mo2_add_filter : add source_group based on the given names +# +# \param:NAME name of the group +# \param:FILES files to add +# \param:GROUPS basename to add, e.g., "foo" will add "foo.cpp", "foo.h" and "foo.inc" +# +function(mo2_add_filter) + cmake_parse_arguments(PARSE_ARGV 0 add_filter "" "NAME" "FILES;GROUPS") + + set(files ${add_filter_FILES}) + + foreach(f ${add_filter_GROUPS}) + set(files ${files} ${f}.cpp ${f}.h ${f}.inc) + endforeach() + + string(REPLACE "/" "\\" filter_name ${add_filter_NAME}) + source_group(${filter_name} FILES ${files}) +endfunction() + +#! mo2_deploy_qt_for_tests : add comments to deploy Qt for tests +# +# unlike mo2_deploy_qt(), this function does not perform any cleaning +# +# \param:TARGET name of the target to deploy for +# \param:BINARIES names of the binaries to deploy from +# +function(mo2_deploy_qt_for_tests) + if(NOT WIN32) + return() + endif() + + cmake_parse_arguments(DEPLOY "" "TARGET" "BINARIES" ${ARGN}) + + mo2_find_qt_executable(windeployqt windeployqt) + + add_custom_command(TARGET "${DEPLOY_TARGET}" + POST_BUILD + COMMAND ${windeployqt} + ARGS + --dir $ + --no-translations + --verbose 0 + --no-compiler-runtime + "$" + "${DEPLOY_BINARIES}" + VERBATIM + COMMAND_EXPAND_LISTS + WORKING_DIRECTORY $ + ) +endfunction() + +#! mo2_deploy_qt : add commands to deploy Qt from the given binaries +# +# this function attach install() entries that deploy Qt for the given binaries +# +# \param:NOPLUGINS do not deploy Qt plugins +# \param:DIRECTORY directory, relative to CMAKE_INSTALL_PREFIX, to deploy to, default +# to ${MO2_INSTALL_BIN} +# \param:BINARIES names of the binaries (in the install path) to deploy from +# +function(mo2_deploy_qt) + if(NOT WIN32) + return() + endif() + + cmake_parse_arguments(DEPLOY "NOPLUGINS" "DIRECTORY" "BINARIES" ${ARGN}) + + mo2_set_if_not_defined(DEPLOY_DIRECTORY "${MO2_INSTALL_BIN}") + + mo2_find_qt_executable(windeployqt windeployqt) + + set(args + --no-translations + --verbose 0 + --webenginewidgets + --websockets + --openglwidgets + --libdir dlls + --no-compiler-runtime) + + if(MO2_QT_VERSION VERSION_LESS "6.10.0") + list(APPEND args --networkauth) + endif() + + if(${DEPLOY_NOPLUGINS}) + list(APPEND args --no-plugins) + else() + list(APPEND args --plugindir qtplugins) + endif() + + string(REPLACE ";" " " deploy_qt_args "${args}") + + set(bin "${CMAKE_INSTALL_PREFIX}/${DEPLOY_DIRECTORY}") + + set(deploys "") + foreach(binary ${DEPLOY_BINARIES}) + string(APPEND deploys " + execute_process( + COMMAND \"${windeployqt}\" ${deploy_qt_args} \"${binary}\" + WORKING_DIRECTORY \"${bin}\")") + endforeach() + + install(CODE " + ${deploys} + + file(REMOVE_RECURSE \"${bin}/platforms\") + file(REMOVE_RECURSE \"${bin}/styles\") + file(REMOVE_RECURSE \"${bin}/dlls/imageformats\") + file(REMOVE_RECURSE \"${bin}/dlls/tls\") + ") + + # === Begin Qt6 Fix === + + # until there is a cleaner way to do this with windqtdeploy? + + # subfolder of QtQuick + set(qt6_qtquick_to_remove "") + list(APPEND qt6_qtquick_to_remove + Controls Dialogs Layouts LocalStorage NativeStyle Particles Pdf Scene2D + Scene3D Shapes Templates Timeline tooling VirtualKeyboard Window) + + set(qt6_qtdlls_to_remove "") + list(APPEND qt6_qtdlls_to_remove + 3DAnimation 3DCore 3DExtras 3DInput 3DLogic 3DQuickScene2D 3DRender + Pdf PdfQuick QmlLocalStorage QmlXmlListModel QuickControls2 QuickControls2Impl + QuickDialogs2 QuickDialogs2QuickImpl QuickDialogs2Utils QuickLayouts QuickParticles + QuickShapes QuickTemplates2 QuickTimeline Sql StateMachine StateMachineQml VirtualKeyboard) + + set(removals "") + foreach (qt6_qtquick_removal ${qt6_qtquick_to_remove}) + string(APPEND removals " + file(REMOVE_RECURSE \"${bin}/QtQuick/${qt6_qtquick_removal}\")") + endforeach() + foreach (qt6_dll_removal ${qt6_qtdlls_to_remove}) + string(APPEND removals " + file(REMOVE \"${bin}/dlls/Qt6${qt6_dll_removal}.dll\")") + endforeach() + install(CODE "${removals}") + + # === End Qt6 Fix === + + if(NOT ${DEPLOY_NOPLUGINS}) + set(qtwebengine_process_exe $,QtWebEngineProcessd.exe,QtWebEngineProcess.exe>) + install(CODE " + if(EXISTS \"${bin}/dlls/${qtwebengine_process_exe}\") + message(STATUS \"[MO2] Moving ${qtwebengine_process_exe} to root...\") + file(RENAME \"${bin}/dlls/${qtwebengine_process_exe}\" \"${bin}/${qtwebengine_process_exe}\") + endif() + + foreach(_dir platforms styles) + if(EXISTS \"${bin}/qtplugins/\${_dir}\") + file(REMOVE_RECURSE \"${bin}/\${_dir}\") + file(RENAME \"${bin}/qtplugins/\${_dir}\" \"${bin}/\${_dir}\") + endif() + endforeach() + + foreach(_dir imageformats tls) + if(EXISTS \"${bin}/qtplugins/\${_dir}\") + file(REMOVE_RECURSE \"${bin}/dlls/\${_dir}\") + file(RENAME \"${bin}/qtplugins/\${_dir}\" \"${bin}/dlls/\${_dir}\") + endif() + endforeach() + + file(REMOVE_RECURSE \"${bin}/qtplugins\") + ") + endif() +endfunction() + +#! mo2_add_lupdate : generate .ts files from the given sources +# +# this function adds a ${TARGET}_lupdate target +# +# \param:TARGET target to generate lupdate for +# \param:TS_FILE .ts file to generate +# \param:SOURCES source folders to generate .ts file from +# +function(mo2_add_lupdate TARGET) + cmake_parse_arguments(MO2 "" "TS_FILE" "SOURCES" ${ARGN}) + + set(translation_files "") + set(ui_files "") + + get_property(languages GLOBAL PROPERTY ENABLED_LANGUAGES) + + if ("CXX" IN_LIST languages) + set(is_cpp True) + else() + set(is_cpp False) + endif() + + # we glob the source files for Python plugins to avoid duplicate string in .ui and + # .py, for C++, we will directly use MO2_SOURCES (using translation_files broke + # the modorganizer build, probably because there are too many sources?) + foreach (SOURCE ${MO2_SOURCES}) + if (${is_cpp}) + file(GLOB_RECURSE source_sources CONFIGURE_DEPENDS + ${SOURCE}/*.cpp + ${SOURCE}/*.h) + else() + file(GLOB_RECURSE source_sources CONFIGURE_DEPENDS + ${SOURCE}/*.py) + endif() + + # ui files + file(GLOB_RECURSE source_ui_files CONFIGURE_DEPENDS ${SOURCE}/*.ui) + + list(APPEND ui_files ${source_ui_files}) + list(APPEND translation_files ${source_sources} ${source_ui_files}) + endforeach() + + # for Python, we need to remove the .py generated because these can be generated + # in the source folder + list(TRANSFORM ui_files REPLACE "[.]ui$" ".py") + list(REMOVE_ITEM translation_files ${ui_files}) + + message(TRACE "TS_FILE: ${MO2_TS_FILE}, SOURCES: ${MO2_SOURCES}, FILES: ${translation_files}") + + add_custom_target("${TARGET}_lupdate" DEPENDS ${MO2_TS_FILE}) + + if (${is_cpp}) + mo2_find_qt_executable(lupdate lupdate) + set(lupdate_command ${lupdate} ${MO2_SOURCES} -ts ${MO2_TS_FILE}) + else() + mo2_python_install_pyqt() + set(lupdate_command + ${CMAKE_COMMAND} + -E env PYTHONPATH=${MO2_PYLIBS_DIR} + ${MO2_PYLIBS_DIR}/bin/pylupdate${MO2_QT_VERSION_MAJOR}.exe + --ts "${MO2_TS_FILE}" ${translation_files}) + + add_dependencies("${TARGET}_lupdate" PyQt6) + endif() + + add_custom_command(OUTPUT ${MO2_TS_FILE} + COMMAND ${lupdate_command} + DEPENDS ${translation_files} + VERBATIM) + + # we need to set this property otherwise there is an issue with C# projects + # requiring nuget packages (e.g., installer_omod) that tries to resolve Nuget + # packages on these target but fails because there are obviously none + # + # we also "hide" the target by moving them to autogen + set_target_properties(${TARGET}_lupdate PROPERTIES + VS_GLOBAL_ResolveNugetPackages False + FOLDER autogen) + +endfunction() + +#! mo2_add_lrelease : generate .ts files from the given sources +# +# this function adds a ${TARGET}_lrelease target +# +# \param:TARGET target to generate releases for +# \param:INSTALL if set, QM files will be installed +# \param:DIRECTORY if INSTALL is set, path where translations should be installed, +# default to ${MO2_INSTALL_BIN}/translations +# \param:QM_FILE .qm file to generate +# \param:TS_FILES source ts +# +function(mo2_add_lrelease TARGET) + cmake_parse_arguments(MO2 "INSTALL" "DIRECTORY;QM_FILE" "TS_FILES" ${ARGN}) + + mo2_set_if_not_defined(MO2_DIRECTORY "${MO2_INSTALL_BIN}/translations") + + mo2_find_qt_executable(lrelease_command lrelease) + + add_custom_command(OUTPUT ${MO2_QM_FILE} + COMMAND ${lrelease_command} + ARGS ${MO2_TS_FILES} -qm ${MO2_QM_FILE} + DEPENDS "${MO2_TS_FILES}" + VERBATIM) + + add_custom_target("${TARGET}_lrelease" DEPENDS ${MO2_QM_FILE}) + + # we need to set this property otherwise there is an issue with C# projects + # requiring nuget packages (e.g., installer_omod) that tries to resolve Nuget + # packages on these target but fails because there are obviously none + # + # we also "hide" the target by moving them to autogen + set_target_properties(${TARGET}_lrelease PROPERTIES + VS_GLOBAL_ResolveNugetPackages False + FOLDER autogen) + + if (${MO2_INSTALL}) + install(FILES ${MO2_QM_FILE} DESTINATION ${MO2_DIRECTORY}) + endif() + +endfunction() + + +#! mo2_add_translations : generate translation files +# +# this function is a wrapper around mo2_add_lupdate and mo2_add_lrelease +# +# \param:TARGET target to generate translations for +# \param:RELEASE if set, will use mo2_add_lrelease to generate .qm files +# \param:INSTALL_RELEASE if true, will install generated .qm files (force RELEASE) +# \param:INSTALL_DIRECTORY installation directory for .qm files, default to ${MO2_INSTALL_BIN}/translations +# \param:TS_FILE intermediate .ts file to generate +# \param:QM_FILE file .qm file to generate if RELEASE or INSTALL_RELEASE is set +# \param:SOURCES source directories to look for translations, send to mo2_add_lupdate +# +function(mo2_add_translations TARGET) + cmake_parse_arguments(MO2 "RELEASE;INSTALL_RELEASE" "TS_FILE;QM_FILE;INSTALL_DIRECTORY" "SOURCES" ${ARGN}) + + mo2_set_if_not_defined(MO2_TS_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET}_en.ts) + mo2_set_if_not_defined(MO2_QM_FILE ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_en.qm) + mo2_set_if_not_defined(MO2_INSTALL_DIRECTORY "${MO2_INSTALL_BIN}/translations") + + # force release with install + if (${MO2_INSTALL_RELEASE}) + set(MO2_RELEASE True) + endif() + + mo2_add_lupdate(${TARGET} TS_FILE ${MO2_TS_FILE} SOURCES ${MO2_SOURCES}) + + if (${MO2_RELEASE}) + mo2_add_lrelease(${TARGET} + INSTALL ${MO2_INSTALL_RELEASE} + DIRECTORY ${MO2_INSTALL_DIRECTORY} + TS_FILES ${MO2_TS_FILE} + QM_FILE ${MO2_QM_FILE}) + + add_dependencies(${TARGET}_lrelease ${TARGET}_lupdate) + add_dependencies(${TARGET} ${TARGET}_lrelease) + else() + add_dependencies(${TARGET} ${TARGET}_lupdate) + endif() + +endfunction() + +set(MO2_UTILS_DEFINED TRUE) diff --git a/libs/cmake_common/mo2_versions.cmake b/libs/cmake_common/mo2_versions.cmake new file mode 100644 index 0000000..ee432f4 --- /dev/null +++ b/libs/cmake_common/mo2_versions.cmake @@ -0,0 +1,49 @@ +cmake_minimum_required(VERSION 3.22) + +if (DEFINED MO2_VERSIONS_INCLUDED) + return() +endif() + +# define MO2_QT_VERSION and related variables +# - if MO2_QT_VERSION is already defined, simply extract the major, minor and patch +# components for +# - otherwise, if the project is a C++ project, look-up Qt and use the version from +# the package found +# - otherwise, or if Qt was not found in the previous step, use a default version +# +if (NOT DEFINED MO2_QT_VERSION) + if (CMAKE_CXX_COMPILER_LOADED) + find_package(Qt6 CONFIG QUIET) + endif() + + if (Qt6_FOUND) + set(MO2_QT_VERSION "${Qt6_VERSION}") + else() + set(MO2_QT_VERSION "6.7.3") + endif() +endif() + +string(REPLACE "." ";" MO2_QT_VERSION_LIST ${MO2_QT_VERSION}) +list(GET MO2_QT_VERSION_LIST 0 MO2_QT_VERSION_MAJOR) +list(GET MO2_QT_VERSION_LIST 1 MO2_QT_VERSION_MINOR) +list(GET MO2_QT_VERSION_LIST 2 MO2_QT_VERSION_PATCH) +unset(MO2_QT_VERSION_LIST) + +message(STATUS "[MO2] Qt version: ${MO2_QT_VERSION} (${MO2_QT_VERSION_MAJOR}, ${MO2_QT_VERSION_MINOR}, ${MO2_QT_VERSION_PATCH})") + +mo2_set_if_not_defined(MO2_PYTHON_VERSION "3.12") + +if (MO2_QT_VERSION_MAJOR EQUAL 6 AND MO2_QT_VERSION_MINOR EQUAL 10) + mo2_set_if_not_defined(MO2_PYQT_VERSION "6.10.2") + mo2_set_if_not_defined(MO2_SIP_VERSION "6.15.1") +else() + mo2_set_if_not_defined(MO2_PYQT_VERSION "6.7.1") + mo2_set_if_not_defined(MO2_SIP_VERSION "6.8.6") +endif() + +message(STATUS "[MO2] Python version: ${MO2_PYTHON_VERSION}") +message(STATUS "[MO2] PyQt version: ${MO2_PYQT_VERSION}") +message(STATUS "[MO2] SIP version: ${MO2_SIP_VERSION}") + +# mark as included +set(MO2_VERSIONS_INCLUDED TRUE) diff --git a/libs/dds-header/include/DDS/DDS.h b/libs/dds-header/include/DDS/DDS.h new file mode 100644 index 0000000..516dc94 --- /dev/null +++ b/libs/dds-header/include/DDS/DDS.h @@ -0,0 +1,331 @@ +//-------------------------------------------------------------------------------------- +// DDS.h +// +// This header defines constants and structures that are useful when parsing +// DDS files. DDS files were originally designed to use several structures +// and constants that are native to DirectDraw and are defined in ddraw.h, +// such as DDSURFACEDESC2 and DDSCAPS2. This file defines similar +// (compatible) constants and structures so that one can use DDS files +// without needing to include ddraw.h. +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// http://go.microsoft.com/fwlink/?LinkId=248926 +// http://go.microsoft.com/fwlink/?LinkId=248929 +// http://go.microsoft.com/fwlink/?LinkID=615561 +//-------------------------------------------------------------------------------------- + +#pragma once + +#include +#include + +namespace DirectX +{ + +#pragma pack(push,1) + + constexpr uint32_t DDS_MAGIC = 0x20534444; // "DDS " + + struct DDS_PIXELFORMAT + { + uint32_t size; + uint32_t flags; + uint32_t fourCC; + uint32_t RGBBitCount; + uint32_t RBitMask; + uint32_t GBitMask; + uint32_t BBitMask; + uint32_t ABitMask; + }; + +#define DDS_FOURCC 0x00000004 // DDPF_FOURCC +#define DDS_RGB 0x00000040 // DDPF_RGB +#define DDS_RGBA 0x00000041 // DDPF_RGB | DDPF_ALPHAPIXELS +#define DDS_LUMINANCE 0x00020000 // DDPF_LUMINANCE +#define DDS_LUMINANCEA 0x00020001 // DDPF_LUMINANCE | DDPF_ALPHAPIXELS +#define DDS_ALPHAPIXELS 0x00000001 // DDPF_ALPHAPIXELS +#define DDS_ALPHA 0x00000002 // DDPF_ALPHA +#define DDS_PAL8 0x00000020 // DDPF_PALETTEINDEXED8 +#define DDS_PAL8A 0x00000021 // DDPF_PALETTEINDEXED8 | DDPF_ALPHAPIXELS +#define DDS_BUMPLUMINANCE 0x00040000 // DDPF_BUMPLUMINANCE +#define DDS_BUMPDUDV 0x00080000 // DDPF_BUMPDUDV +#define DDS_BUMPDUDVA 0x00080001 // DDPF_BUMPDUDV | DDPF_ALPHAPIXELS + +#ifndef MAKEFOURCC +#define MAKEFOURCC(ch0, ch1, ch2, ch3) \ + (static_cast(static_cast(ch0)) \ + | (static_cast(static_cast(ch1)) << 8) \ + | (static_cast(static_cast(ch2)) << 16) \ + | (static_cast(static_cast(ch3)) << 24)) +#endif /* MAKEFOURCC */ + +#ifndef DDSGLOBALCONST +#if defined(__GNUC__) && !defined(__MINGW32__) +#define DDSGLOBALCONST extern const __attribute__((weak)) +#else +#define DDSGLOBALCONST extern const __declspec(selectany) +#endif +#endif /* DDSGLOBALCONST */ + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_DXT1 = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('D','X','T','1'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_DXT2 = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('D','X','T','2'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_DXT3 = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('D','X','T','3'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_DXT4 = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('D','X','T','4'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_DXT5 = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('D','X','T','5'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_BC4_UNORM = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('B','C','4','U'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_BC4_SNORM = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('B','C','4','S'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_BC5_UNORM = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('B','C','5','U'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_BC5_SNORM = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('B','C','5','S'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_R8G8_B8G8 = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('R','G','B','G'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_G8R8_G8B8 = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('G','R','G','B'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_YUY2 = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('Y','U','Y','2'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_UYVY = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('U','Y','V','Y'), 0, 0, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A8R8G8B8 = + { sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_X8R8G8B8 = + { sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A8B8G8R8 = + { sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_X8B8G8R8 = + { sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_G16R16 = + { sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 32, 0x0000ffff, 0xffff0000, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_R5G6B5 = + { sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 16, 0xf800, 0x07e0, 0x001f, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A1R5G5B5 = + { sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 16, 0x7c00, 0x03e0, 0x001f, 0x8000 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_X1R5G5B5 = + { sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 16, 0x7c00, 0x03e0, 0x001f, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A4R4G4B4 = + { sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 16, 0x0f00, 0x00f0, 0x000f, 0xf000 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_X4R4G4B4 = + { sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 16, 0x0f00, 0x00f0, 0x000f, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_R8G8B8 = + { sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 24, 0xff0000, 0x00ff00, 0x0000ff, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A8R3G3B2 = + { sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 16, 0x00e0, 0x001c, 0x0003, 0xff00 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_R3G3B2 = + { sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 8, 0xe0, 0x1c, 0x03, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A4L4 = + { sizeof(DDS_PIXELFORMAT), DDS_LUMINANCEA, 0, 8, 0x0f, 0, 0, 0xf0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_L8 = + { sizeof(DDS_PIXELFORMAT), DDS_LUMINANCE, 0, 8, 0xff, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_L16 = + { sizeof(DDS_PIXELFORMAT), DDS_LUMINANCE, 0, 16, 0xffff, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A8L8 = + { sizeof(DDS_PIXELFORMAT), DDS_LUMINANCEA, 0, 16, 0x00ff, 0, 0, 0xff00 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A8L8_ALT = + { sizeof(DDS_PIXELFORMAT), DDS_LUMINANCEA, 0, 8, 0x00ff, 0, 0, 0xff00 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_L8_NVTT1 = + { sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 8, 0xff, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_L16_NVTT1 = + { sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 16, 0xffff, 0, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A8L8_NVTT1 = + { sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 16, 0x00ff, 0, 0, 0xff00 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A8 = + { sizeof(DDS_PIXELFORMAT), DDS_ALPHA, 0, 8, 0, 0, 0, 0xff }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_V8U8 = + { sizeof(DDS_PIXELFORMAT), DDS_BUMPDUDV, 0, 16, 0x00ff, 0xff00, 0, 0 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_Q8W8V8U8 = + { sizeof(DDS_PIXELFORMAT), DDS_BUMPDUDV, 0, 32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000 }; + + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_V16U16 = + { sizeof(DDS_PIXELFORMAT), DDS_BUMPDUDV, 0, 32, 0x0000ffff, 0xffff0000, 0, 0 }; + +// D3DFMT_A2R10G10B10/D3DFMT_A2B10G10R10 should be written using DX10 extension to avoid D3DX 10:10:10:2 reversal issue + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A2R10G10B10 = + { sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 32, 0x000003ff, 0x000ffc00, 0x3ff00000, 0xc0000000 }; + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A2B10G10R10 = + { sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 32, 0x3ff00000, 0x000ffc00, 0x000003ff, 0xc0000000 }; + +// The following legacy Direct3D 9 formats use 'mixed' signed & unsigned channels so requires special handling + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_A2W10V10U10 = + { sizeof(DDS_PIXELFORMAT), DDS_BUMPDUDVA, 0, 32, 0x3ff00000, 0x000ffc00, 0x000003ff, 0xc0000000 }; + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_L6V5U5 = + { sizeof(DDS_PIXELFORMAT), DDS_BUMPLUMINANCE, 0, 16, 0x001f, 0x03e0, 0xfc00, 0 }; + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_X8L8V8U8 = + { sizeof(DDS_PIXELFORMAT), DDS_BUMPLUMINANCE, 0, 32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0 }; + +// This indicates the DDS_HEADER_DXT10 extension is present (the format is in dxgiFormat) + DDSGLOBALCONST DDS_PIXELFORMAT DDSPF_DX10 = + { sizeof(DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('D','X','1','0'), 0, 0, 0, 0, 0 }; + +#define DDS_HEADER_FLAGS_TEXTURE 0x00001007 // DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT +#define DDS_HEADER_FLAGS_MIPMAP 0x00020000 // DDSD_MIPMAPCOUNT +#define DDS_HEADER_FLAGS_VOLUME 0x00800000 // DDSD_DEPTH +#define DDS_HEADER_FLAGS_PITCH 0x00000008 // DDSD_PITCH +#define DDS_HEADER_FLAGS_LINEARSIZE 0x00080000 // DDSD_LINEARSIZE + +#define DDS_HEIGHT 0x00000002 // DDSD_HEIGHT +#define DDS_WIDTH 0x00000004 // DDSD_WIDTH + +#define DDS_SURFACE_FLAGS_TEXTURE 0x00001000 // DDSCAPS_TEXTURE +#define DDS_SURFACE_FLAGS_MIPMAP 0x00400008 // DDSCAPS_COMPLEX | DDSCAPS_MIPMAP +#define DDS_SURFACE_FLAGS_CUBEMAP 0x00000008 // DDSCAPS_COMPLEX + +#define DDS_CUBEMAP_POSITIVEX 0x00000600 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX +#define DDS_CUBEMAP_NEGATIVEX 0x00000a00 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX +#define DDS_CUBEMAP_POSITIVEY 0x00001200 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY +#define DDS_CUBEMAP_NEGATIVEY 0x00002200 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY +#define DDS_CUBEMAP_POSITIVEZ 0x00004200 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ +#define DDS_CUBEMAP_NEGATIVEZ 0x00008200 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ + +#define DDS_CUBEMAP_ALLFACES ( DDS_CUBEMAP_POSITIVEX | DDS_CUBEMAP_NEGATIVEX |\ + DDS_CUBEMAP_POSITIVEY | DDS_CUBEMAP_NEGATIVEY |\ + DDS_CUBEMAP_POSITIVEZ | DDS_CUBEMAP_NEGATIVEZ ) + +#define DDS_CUBEMAP 0x00000200 // DDSCAPS2_CUBEMAP + +#define DDS_FLAGS_VOLUME 0x00200000 // DDSCAPS2_VOLUME + +// Subset here matches D3D10_RESOURCE_DIMENSION and D3D11_RESOURCE_DIMENSION + enum DDS_RESOURCE_DIMENSION : uint32_t + { + DDS_DIMENSION_TEXTURE1D = 2, + DDS_DIMENSION_TEXTURE2D = 3, + DDS_DIMENSION_TEXTURE3D = 4, + }; + + // Subset here matches D3D10_RESOURCE_MISC_FLAG and D3D11_RESOURCE_MISC_FLAG + enum DDS_RESOURCE_MISC_FLAG : uint32_t + { + DDS_RESOURCE_MISC_TEXTURECUBE = 0x4L, + }; + + enum DDS_MISC_FLAGS2 : uint32_t + { + DDS_MISC_FLAGS2_ALPHA_MODE_MASK = 0x7L, + }; + +#ifndef DDS_ALPHA_MODE_DEFINED +#define DDS_ALPHA_MODE_DEFINED + enum DDS_ALPHA_MODE : uint32_t + { + DDS_ALPHA_MODE_UNKNOWN = 0, + DDS_ALPHA_MODE_STRAIGHT = 1, + DDS_ALPHA_MODE_PREMULTIPLIED = 2, + DDS_ALPHA_MODE_OPAQUE = 3, + DDS_ALPHA_MODE_CUSTOM = 4, + }; +#endif + + struct DDS_HEADER + { + uint32_t size; + uint32_t flags; + uint32_t height; + uint32_t width; + uint32_t pitchOrLinearSize; + uint32_t depth; // only if DDS_HEADER_FLAGS_VOLUME is set in flags + uint32_t mipMapCount; + uint32_t reserved1[11]; + DDS_PIXELFORMAT ddspf; + uint32_t caps; + uint32_t caps2; + uint32_t caps3; + uint32_t caps4; + uint32_t reserved2; + }; + + struct DDS_HEADER_DXT10 + { + DXGI_FORMAT dxgiFormat; + uint32_t resourceDimension; + uint32_t miscFlag; // see D3D11_RESOURCE_MISC_FLAG + uint32_t arraySize; + uint32_t miscFlags2; // see DDS_MISC_FLAGS2 + }; + +#pragma pack(pop) + + static_assert(sizeof(DDS_PIXELFORMAT) == 32, "DDS pixel format size mismatch"); + static_assert(sizeof(DDS_HEADER) == 124, "DDS Header size mismatch"); + static_assert(sizeof(DDS_HEADER_DXT10) == 20, "DDS DX10 Extended Header size mismatch"); + + constexpr size_t DDS_MIN_HEADER_SIZE = sizeof(uint32_t) + sizeof(DDS_HEADER); + constexpr size_t DDS_DX10_HEADER_SIZE = sizeof(uint32_t) + sizeof(DDS_HEADER) + sizeof(DDS_HEADER_DXT10); + static_assert(DDS_DX10_HEADER_SIZE > DDS_MIN_HEADER_SIZE, "DDS DX10 Header should be larger than standard header"); + +} // namespace + +namespace Xbox +{ + DDSGLOBALCONST DirectX::DDS_PIXELFORMAT DDSPF_XBOX = + { sizeof(DirectX::DDS_PIXELFORMAT), DDS_FOURCC, MAKEFOURCC('X','B','O','X'), 0, 0, 0, 0, 0 }; + +#pragma pack(push,1) + + struct DDS_HEADER_XBOX + // Must match structure in XboxDDSTextureLoader module + { + DXGI_FORMAT dxgiFormat; + uint32_t resourceDimension; + uint32_t miscFlag; // see DDS_RESOURCE_MISC_FLAG + uint32_t arraySize; + uint32_t miscFlags2; // see DDS_MISC_FLAGS2 + uint32_t tileMode; // see XG_TILE_MODE / XG_SWIZZLE_MODE + uint32_t baseAlignment; + uint32_t dataSize; + uint32_t xdkVer; // matching _XDK_VER / _GXDK_VER + }; + +#pragma pack(pop) + + static_assert(sizeof(DDS_HEADER_XBOX) == 36, "DDS XBOX Header size mismatch"); + static_assert(sizeof(DDS_HEADER_XBOX) > sizeof(DirectX::DDS_HEADER_DXT10), "DDS XBOX Header should be larger than DX10 header"); + + constexpr size_t DDS_XBOX_HEADER_SIZE = sizeof(uint32_t) + sizeof(DirectX::DDS_HEADER) + sizeof(DDS_HEADER_XBOX); + + constexpr uint32_t XBOX_TILEMODE_SCARLETT = 0x1000000; +} // namespace diff --git a/libs/dds-header/include/DDS/dxgiformat.h b/libs/dds-header/include/DDS/dxgiformat.h new file mode 100644 index 0000000..3cb5f9e --- /dev/null +++ b/libs/dds-header/include/DDS/dxgiformat.h @@ -0,0 +1,95 @@ +// Minimal dxgiformat.h for Linux - DXGI_FORMAT enum values from Microsoft DirectX SDK +// These are public, well-documented constants. Only includes values used by MO2/bsatk. +// Full spec: https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format + +#pragma once + +typedef enum DXGI_FORMAT { + DXGI_FORMAT_UNKNOWN = 0, + DXGI_FORMAT_R32G32B32A32_TYPELESS = 1, + DXGI_FORMAT_R32G32B32A32_FLOAT = 2, + DXGI_FORMAT_R32G32B32A32_UINT = 3, + DXGI_FORMAT_R32G32B32A32_SINT = 4, + DXGI_FORMAT_R32G32B32_TYPELESS = 5, + DXGI_FORMAT_R32G32B32_FLOAT = 6, + DXGI_FORMAT_R32G32B32_UINT = 7, + DXGI_FORMAT_R32G32B32_SINT = 8, + DXGI_FORMAT_R16G16B16A16_TYPELESS = 9, + DXGI_FORMAT_R16G16B16A16_FLOAT = 10, + DXGI_FORMAT_R16G16B16A16_UNORM = 11, + DXGI_FORMAT_R16G16B16A16_UINT = 12, + DXGI_FORMAT_R16G16B16A16_SNORM = 13, + DXGI_FORMAT_R16G16B16A16_SINT = 14, + DXGI_FORMAT_R32G32_TYPELESS = 15, + DXGI_FORMAT_R32G32_FLOAT = 16, + DXGI_FORMAT_R32G32_UINT = 17, + DXGI_FORMAT_R32G32_SINT = 18, + DXGI_FORMAT_R10G10B10A2_TYPELESS = 23, + DXGI_FORMAT_R10G10B10A2_UNORM = 24, + DXGI_FORMAT_R10G10B10A2_UINT = 25, + DXGI_FORMAT_R8G8B8A8_TYPELESS = 27, + DXGI_FORMAT_R8G8B8A8_UNORM = 28, + DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29, + DXGI_FORMAT_R8G8B8A8_UINT = 30, + DXGI_FORMAT_R8G8B8A8_SNORM = 31, + DXGI_FORMAT_R8G8B8A8_SINT = 32, + DXGI_FORMAT_R16G16_TYPELESS = 33, + DXGI_FORMAT_R16G16_FLOAT = 34, + DXGI_FORMAT_R16G16_UNORM = 35, + DXGI_FORMAT_R16G16_UINT = 36, + DXGI_FORMAT_R16G16_SNORM = 37, + DXGI_FORMAT_R16G16_SINT = 38, + DXGI_FORMAT_R32_TYPELESS = 39, + DXGI_FORMAT_D32_FLOAT = 40, + DXGI_FORMAT_R32_FLOAT = 41, + DXGI_FORMAT_R32_UINT = 42, + DXGI_FORMAT_R32_SINT = 43, + DXGI_FORMAT_R8G8_TYPELESS = 48, + DXGI_FORMAT_R8G8_UNORM = 49, + DXGI_FORMAT_R8G8_UINT = 50, + DXGI_FORMAT_R8G8_SNORM = 51, + DXGI_FORMAT_R8G8_SINT = 52, + DXGI_FORMAT_R16_TYPELESS = 53, + DXGI_FORMAT_R16_FLOAT = 54, + DXGI_FORMAT_D16_UNORM = 55, + DXGI_FORMAT_R16_UNORM = 56, + DXGI_FORMAT_R16_UINT = 57, + DXGI_FORMAT_R16_SNORM = 58, + DXGI_FORMAT_R16_SINT = 59, + DXGI_FORMAT_R8_TYPELESS = 60, + DXGI_FORMAT_R8_UNORM = 61, + DXGI_FORMAT_R8_UINT = 62, + DXGI_FORMAT_R8_SNORM = 63, + DXGI_FORMAT_R8_SINT = 64, + DXGI_FORMAT_A8_UNORM = 65, + DXGI_FORMAT_BC1_TYPELESS = 70, + DXGI_FORMAT_BC1_UNORM = 71, + DXGI_FORMAT_BC1_UNORM_SRGB = 72, + DXGI_FORMAT_BC2_TYPELESS = 73, + DXGI_FORMAT_BC2_UNORM = 74, + DXGI_FORMAT_BC2_UNORM_SRGB = 75, + DXGI_FORMAT_BC3_TYPELESS = 76, + DXGI_FORMAT_BC3_UNORM = 77, + DXGI_FORMAT_BC3_UNORM_SRGB = 78, + DXGI_FORMAT_BC4_TYPELESS = 79, + DXGI_FORMAT_BC4_UNORM = 80, + DXGI_FORMAT_BC4_SNORM = 81, + DXGI_FORMAT_BC5_TYPELESS = 82, + DXGI_FORMAT_BC5_UNORM = 83, + DXGI_FORMAT_BC5_SNORM = 84, + DXGI_FORMAT_B5G6R5_UNORM = 85, + DXGI_FORMAT_B5G5R5A1_UNORM = 86, + DXGI_FORMAT_B8G8R8A8_UNORM = 87, + DXGI_FORMAT_B8G8R8X8_UNORM = 88, + DXGI_FORMAT_B8G8R8A8_TYPELESS = 90, + DXGI_FORMAT_B8G8R8A8_UNORM_SRGB = 91, + DXGI_FORMAT_B8G8R8X8_TYPELESS = 92, + DXGI_FORMAT_B8G8R8X8_UNORM_SRGB = 93, + DXGI_FORMAT_BC6H_TYPELESS = 94, + DXGI_FORMAT_BC6H_UF16 = 95, + DXGI_FORMAT_BC6H_SF16 = 96, + DXGI_FORMAT_BC7_TYPELESS = 97, + DXGI_FORMAT_BC7_UNORM = 98, + DXGI_FORMAT_BC7_UNORM_SRGB = 99, + DXGI_FORMAT_FORCE_UINT = 0xffffffff +} DXGI_FORMAT; diff --git a/libs/dds-header/mo2-dds-header-config.cmake b/libs/dds-header/mo2-dds-header-config.cmake new file mode 100644 index 0000000..112d125 --- /dev/null +++ b/libs/dds-header/mo2-dds-header-config.cmake @@ -0,0 +1,6 @@ +if (TARGET mo2::dds-header) + return() +endif() + +add_library(mo2::dds-header INTERFACE IMPORTED) +target_include_directories(mo2::dds-header INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include/DDS) diff --git a/libs/dds_header/.editorconfig b/libs/dds_header/.editorconfig new file mode 100644 index 0000000..f64592a --- /dev/null +++ b/libs/dds_header/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.py] +indent_style = space +indent_size = 4 + +[{CMakeLists.txt, *.cmake}] +indent_style = space +indent_size = 4 diff --git a/libs/dds_header/.gitignore b/libs/dds_header/.gitignore new file mode 100644 index 0000000..5afe969 --- /dev/null +++ b/libs/dds_header/.gitignore @@ -0,0 +1,109 @@ +# Mod Organizer Umbrella stuff +/msbuild.log +/*std*.log +/*build + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/libs/dds_header/.pre-commit-config.yaml b/libs/dds_header/.pre-commit-config.yaml new file mode 100644 index 0000000..4b7d480 --- /dev/null +++ b/libs/dds_header/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/dds_header/CMakeLists.txt b/libs/dds_header/CMakeLists.txt new file mode 100644 index 0000000..a9ad8a8 --- /dev/null +++ b/libs/dds_header/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(DDSPreview LANGUAGES NONE) + +add_subdirectory(src) diff --git a/libs/dds_header/CMakePresets.json b/libs/dds_header/CMakePresets.json new file mode 100644 index 0000000..7726456 --- /dev/null +++ b/libs/dds_header/CMakePresets.json @@ -0,0 +1,11 @@ +{ + "configurePresets": [ + { + "binaryDir": "${sourceDir}/vsbuild", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "generator": "Visual Studio 17 2022", + "name": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/dds_header/LICENSE b/libs/dds_header/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/libs/dds_header/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/libs/dds_header/README.md b/libs/dds_header/README.md new file mode 100644 index 0000000..02ff9a7 --- /dev/null +++ b/libs/dds_header/README.md @@ -0,0 +1,2 @@ +# modorganizer-preview_dds +DDS preview plugin for Mod Organizer 2. diff --git a/libs/dds_header/src/CMakeLists.txt b/libs/dds_header/src/CMakeLists.txt new file mode 100644 index 0000000..f4f0587 --- /dev/null +++ b/libs/dds_header/src/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(mo2-cmake CONFIG REQUIRED) + +add_custom_target(DDSPreview ALL) +mo2_configure_python(DDSPreview SIMPLE) diff --git a/libs/dds_header/src/DDS/DDSDefinitions.py b/libs/dds_header/src/DDS/DDSDefinitions.py new file mode 100644 index 0000000..2d44812 --- /dev/null +++ b/libs/dds_header/src/DDS/DDSDefinitions.py @@ -0,0 +1,808 @@ +import dataclasses +from enum import IntEnum, IntFlag, auto +import struct +from typing import ClassVar, List + +from .glstuff import GL_IMAGE_FORMAT, CompressedGLTextureFormat, UncompressedGLTextureFormat + +DDS_MAGIC_NUMBER = b"DDS " # b"\x20\x53\x44\x44" + + +class IntEnumFromZero(IntEnum): + def _generate_next_value_(name, start, count, last_values): + return count + + +class D3D10_RESOURCE_DIMENSION(IntEnumFromZero): + D3D10_RESOURCE_DIMENSION_UNKNOWN = auto() + D3D10_RESOURCE_DIMENSION_BUFFER = auto() + D3D10_RESOURCE_DIMENSION_TEXTURE1D = auto() + D3D10_RESOURCE_DIMENSION_TEXTURE2D = auto() + D3D10_RESOURCE_DIMENSION_TEXTURE3D = auto() + + +class DXGI_FORMAT(IntEnumFromZero): + DXGI_FORMAT_UNKNOWN = auto() + DXGI_FORMAT_R32G32B32A32_TYPELESS = auto() + DXGI_FORMAT_R32G32B32A32_FLOAT = auto() + DXGI_FORMAT_R32G32B32A32_UINT = auto() + DXGI_FORMAT_R32G32B32A32_SINT = auto() + DXGI_FORMAT_R32G32B32_TYPELESS = auto() + DXGI_FORMAT_R32G32B32_FLOAT = auto() + DXGI_FORMAT_R32G32B32_UINT = auto() + DXGI_FORMAT_R32G32B32_SINT = auto() + DXGI_FORMAT_R16G16B16A16_TYPELESS = auto() + DXGI_FORMAT_R16G16B16A16_FLOAT = auto() + DXGI_FORMAT_R16G16B16A16_UNORM = auto() + DXGI_FORMAT_R16G16B16A16_UINT = auto() + DXGI_FORMAT_R16G16B16A16_SNORM = auto() + DXGI_FORMAT_R16G16B16A16_SINT = auto() + DXGI_FORMAT_R32G32_TYPELESS = auto() + DXGI_FORMAT_R32G32_FLOAT = auto() + DXGI_FORMAT_R32G32_UINT = auto() + DXGI_FORMAT_R32G32_SINT = auto() + DXGI_FORMAT_R32G8X24_TYPELESS = auto() + DXGI_FORMAT_D32_FLOAT_S8X24_UINT = auto() + DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS = auto() + DXGI_FORMAT_X32_TYPELESS_G8X24_UINT = auto() + DXGI_FORMAT_R10G10B10A2_TYPELESS = auto() + DXGI_FORMAT_R10G10B10A2_UNORM = auto() + DXGI_FORMAT_R10G10B10A2_UINT = auto() + DXGI_FORMAT_R11G11B10_FLOAT = auto() + DXGI_FORMAT_R8G8B8A8_TYPELESS = auto() + DXGI_FORMAT_R8G8B8A8_UNORM = auto() + DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = auto() + DXGI_FORMAT_R8G8B8A8_UINT = auto() + DXGI_FORMAT_R8G8B8A8_SNORM = auto() + DXGI_FORMAT_R8G8B8A8_SINT = auto() + DXGI_FORMAT_R16G16_TYPELESS = auto() + DXGI_FORMAT_R16G16_FLOAT = auto() + DXGI_FORMAT_R16G16_UNORM = auto() + DXGI_FORMAT_R16G16_UINT = auto() + DXGI_FORMAT_R16G16_SNORM = auto() + DXGI_FORMAT_R16G16_SINT = auto() + DXGI_FORMAT_R32_TYPELESS = auto() + DXGI_FORMAT_D32_FLOAT = auto() + DXGI_FORMAT_R32_FLOAT = auto() + DXGI_FORMAT_R32_UINT = auto() + DXGI_FORMAT_R32_SINT = auto() + DXGI_FORMAT_R24G8_TYPELESS = auto() + DXGI_FORMAT_D24_UNORM_S8_UINT = auto() + DXGI_FORMAT_R24_UNORM_X8_TYPELESS = auto() + DXGI_FORMAT_X24_TYPELESS_G8_UINT = auto() + DXGI_FORMAT_R8G8_TYPELESS = auto() + DXGI_FORMAT_R8G8_UNORM = auto() + DXGI_FORMAT_R8G8_UINT = auto() + DXGI_FORMAT_R8G8_SNORM = auto() + DXGI_FORMAT_R8G8_SINT = auto() + DXGI_FORMAT_R16_TYPELESS = auto() + DXGI_FORMAT_R16_FLOAT = auto() + DXGI_FORMAT_D16_UNORM = auto() + DXGI_FORMAT_R16_UNORM = auto() + DXGI_FORMAT_R16_UINT = auto() + DXGI_FORMAT_R16_SNORM = auto() + DXGI_FORMAT_R16_SINT = auto() + DXGI_FORMAT_R8_TYPELESS = auto() + DXGI_FORMAT_R8_UNORM = auto() + DXGI_FORMAT_R8_UINT = auto() + DXGI_FORMAT_R8_SNORM = auto() + DXGI_FORMAT_R8_SINT = auto() + DXGI_FORMAT_A8_UNORM = auto() + DXGI_FORMAT_R1_UNORM = auto() + DXGI_FORMAT_R9G9B9E5_SHAREDEXP = auto() + DXGI_FORMAT_R8G8_B8G8_UNORM = auto() + DXGI_FORMAT_G8R8_G8B8_UNORM = auto() + DXGI_FORMAT_BC1_TYPELESS = auto() + DXGI_FORMAT_BC1_UNORM = auto() + DXGI_FORMAT_BC1_UNORM_SRGB = auto() + DXGI_FORMAT_BC2_TYPELESS = auto() + DXGI_FORMAT_BC2_UNORM = auto() + DXGI_FORMAT_BC2_UNORM_SRGB = auto() + DXGI_FORMAT_BC3_TYPELESS = auto() + DXGI_FORMAT_BC3_UNORM = auto() + DXGI_FORMAT_BC3_UNORM_SRGB = auto() + DXGI_FORMAT_BC4_TYPELESS = auto() + DXGI_FORMAT_BC4_UNORM = auto() + DXGI_FORMAT_BC4_SNORM = auto() + DXGI_FORMAT_BC5_TYPELESS = auto() + DXGI_FORMAT_BC5_UNORM = auto() + DXGI_FORMAT_BC5_SNORM = auto() + DXGI_FORMAT_B5G6R5_UNORM = auto() + DXGI_FORMAT_B5G5R5A1_UNORM = auto() + DXGI_FORMAT_B8G8R8A8_UNORM = auto() + DXGI_FORMAT_B8G8R8X8_UNORM = auto() + DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM = auto() + DXGI_FORMAT_B8G8R8A8_TYPELESS = auto() + DXGI_FORMAT_B8G8R8A8_UNORM_SRGB = auto() + DXGI_FORMAT_B8G8R8X8_TYPELESS = auto() + DXGI_FORMAT_B8G8R8X8_UNORM_SRGB = auto() + DXGI_FORMAT_BC6H_TYPELESS = auto() + DXGI_FORMAT_BC6H_UF16 = auto() + DXGI_FORMAT_BC6H_SF16 = auto() + DXGI_FORMAT_BC7_TYPELESS = auto() + DXGI_FORMAT_BC7_UNORM = auto() + DXGI_FORMAT_BC7_UNORM_SRGB = auto() + DXGI_FORMAT_AYUV = auto() + DXGI_FORMAT_Y410 = auto() + DXGI_FORMAT_Y416 = auto() + DXGI_FORMAT_NV12 = auto() + DXGI_FORMAT_P010 = auto() + DXGI_FORMAT_P016 = auto() + DXGI_FORMAT_420_OPAQUE = auto() + DXGI_FORMAT_YUY2 = auto() + DXGI_FORMAT_Y210 = auto() + DXGI_FORMAT_Y216 = auto() + DXGI_FORMAT_NV11 = auto() + DXGI_FORMAT_AI44 = auto() + DXGI_FORMAT_IA44 = auto() + DXGI_FORMAT_P8 = auto() + DXGI_FORMAT_A8P8 = auto() + DXGI_FORMAT_B4G4R4A4_UNORM = auto() + DXGI_FORMAT_P208 = auto() + DXGI_FORMAT_V208 = auto() + DXGI_FORMAT_V408 = auto() + DXGI_FORMAT_FORCE_UINT = auto() + + +def DataclassFromBytes(dataclass): + class LoadableDataclass(dataclass): + def __init__(self, bytes=None): + super(LoadableDataclass, self).__init__() + if bytes: + self.fromBytes(bytes) + + def fromStream(self, byteStream): + self.fromBytes(byteStream.read(struct.calcsize(self.structFormat))) + + def fromBytes(self, bytes): + loaded = struct.unpack(self.structFormat, bytes) + fields = dataclasses.fields(self) + memberIndex = 0 + for field in fields: + if field.metadata and "count" in field.metadata: + # We have a list + listed = field.type.__args__[0] + myList = [] + for i in range(field.metadata["count"]): + myList.append(listed(loaded[memberIndex])) + memberIndex += 1 + self.__dict__[field.name] = myList + else: + self.__dict__[field.name] = field.type(loaded[memberIndex]) + memberIndex += 1 + + return LoadableDataclass + + +@DataclassFromBytes +@dataclasses.dataclass +class DDS_PIXELFORMAT: + structFormat: ClassVar[str] = "> rShift) * multiplier) // rDivisor + else: + red = 0 + if gIntMask: + green = (((gIntMask & pixel) >> gShift) * multiplier) // gDivisor + else: + green = 0 + if bIntMask: + blue = (((bIntMask & pixel) >> bShift) * multiplier) // bDivisor + else: + blue = 0 + if aIntMask: + alpha = (((aIntMask & pixel) >> aShift) * multiplier) // aDivisor + else: + alpha = multiplier + struct.pack_into(packFormat, repacked, repackIndex, blue, green, red, alpha) + repackIndex += biggestComponent * 4 + return bytes(repacked) + + return (convert, glInternalFormat, glFormat, glType) + + +def getGLFormat(pixelFormat, dxt10Header=None): + # Half or more of this function may be unreachable or otherwise redundant. + glInternalFormat = None + dxgiFormat = None + if dxt10Header: + dxgiFormat = dxt10Header.dxgiFormat + flags = pixelFormat.dwFlags + if flags & pixelFormat.Flags.DDPF_FOURCC: + fourCC = pixelFormat.dwFourCC + if fourCC == b"DX10" and not dxt10Header: + raise UnsupportedDDSFormatException() + dxgiFormat = dxt10Header.dxgiFormat if dxt10Header else fourCCToDXGI(fourCC) + + if dxgiFormat: + return dxgiToGL[dxgiFormat] + elif flags & ( + pixelFormat.Flags.DDPF_ALPHA | pixelFormat.Flags.DDPF_RGB | pixelFormat.Flags.DDPF_YUV | pixelFormat.Flags.DDPF_LUMINANCE): + compressed = False + + rBitmask = None + gBitmask = None + bBitmask = None + aBitmask = None + lumBitmask = None + if flags & (pixelFormat.Flags.DDPF_ALPHA | pixelFormat.Flags.DDPF_ALPHAPIXELS): + aBitmask = pixelFormat.dwABitMask + if flags & (pixelFormat.Flags.DDPF_RGB | pixelFormat.Flags.DDPF_YUV): + rBitmask = pixelFormat.dwRBitMask + gBitmask = pixelFormat.dwGBitMask + bBitmask = pixelFormat.dwBBitMask + if flags & pixelFormat.Flags.DDPF_LUMINANCE: + lumBitmask = pixelFormat.dwRBitMask + + def bitCount(theBytes): + count = 0 + for byte in theBytes: + count += bin(byte).count("1") + return count + + def firstBit(theBytes): + index = 0 + for byte in theBytes: + if byte != 0: + return index + format(byte, 'b').find("1") + index += 8 + + bitCounts = dict() + starts = dict() + namedBitmasks = dict() + if rBitmask: + bitCounts["r"] = bitCount(rBitmask) + starts["r"] = firstBit(rBitmask) + namedBitmasks["r"] = rBitmask + if gBitmask: + bitCounts["g"] = bitCount(gBitmask) + starts["g"] = firstBit(gBitmask) + namedBitmasks["g"] = gBitmask + if bBitmask: + bitCounts["b"] = bitCount(bBitmask) + starts["b"] = firstBit(bBitmask) + namedBitmasks["b"] = bBitmask + if aBitmask: + bitCounts["a"] = bitCount(aBitmask) + starts["a"] = firstBit(aBitmask) + namedBitmasks["a"] = aBitmask + if lumBitmask: + bitCounts["luminance"] = bitCount(lumBitmask) + starts["luminance"] = firstBit(lumBitmask) + namedBitmasks["luminance"] = lumBitmask + + toSort = [] + for key in starts: + toSort.append((starts[key], key)) + toSort.sort() + + glInternalFormatName = ["GL"] + glRequirements = None + + desc = "GL_" + lastBitCount = -1 + usedBitCounts = set() + for pos, channel in toSort: + if lastBitCount != bitCounts[channel]: + if lastBitCount != -1: + desc += str(lastBitCount) + lastBitCount = bitCounts[channel] + usedBitCounts.add(lastBitCount) + desc += channel + if len(usedBitCounts) != 1: + desc += str(lastBitCount) + + desc = desc.upper() + glFormat = GL_IMAGE_FORMAT[desc] if desc in GL_IMAGE_FORMAT.__members__ else None + + numComponents = len(toSort) + byteCount = (pixelFormat.dwRGBBitCount + 7) // 8 + needsConversion = False + if len(usedBitCounts) == 1: + if lastBitCount == 8: + glType = GL_IMAGE_FORMAT.GL_UNSIGNED_BYTE + elif lastBitCount == 16: + glType = GL_IMAGE_FORMAT.GL_UNSIGNED_SHORT + elif lastBitCount == 32: + glType = GL_IMAGE_FORMAT.GL_UNSIGNED_INT + elif lastBitCount == 4: + if numComponents == 4: + glType = GL_IMAGE_FORMAT.GL_UNSIGNED_SHORT_4_4_4_4_REV + else: + needsConversion = True + if not glFormat or numComponents * lastBitCount != pixelFormat.dwRGBBitCount: + needsConversion = True + + if needsConversion: + convert, glInternalFormat, glFormat, glType = buildConverter(byteCount, bitmasks=namedBitmasks) + else: + convert = None + + if not glInternalFormat: + glInternalFormatName = "_".join(glInternalFormatName) + glInternalFormat = GL_IMAGE_FORMAT[ + glInternalFormatName] if glInternalFormatName in GL_IMAGE_FORMAT.__members__ else GL_IMAGE_FORMAT.GL_RGBA + + if compressed: + return CompressedGLTextureFormat(glRequirements, glInternalFormat) + else: + return UncompressedGLTextureFormat(glRequirements, glInternalFormat, glFormat, glType, convert) + + +def sizeFromFormat(dxgiFormat, width, height): + blockCompressed = False + if dxgiFormat in {DXGI_FORMAT.DXGI_FORMAT_BC1_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB, + DXGI_FORMAT.DXGI_FORMAT_BC4_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM}: + blockCompressed = True + blockSize = 8 + elif dxgiFormat in {DXGI_FORMAT.DXGI_FORMAT_BC2_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB, + DXGI_FORMAT.DXGI_FORMAT_BC3_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB, + DXGI_FORMAT.DXGI_FORMAT_BC5_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM, + DXGI_FORMAT.DXGI_FORMAT_BC6H_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC6H_UF16, + DXGI_FORMAT.DXGI_FORMAT_BC6H_SF16, + DXGI_FORMAT.DXGI_FORMAT_BC7_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM_SRGB}: + blockCompressed = True + blockSize = 16 + + if blockCompressed: + return max(1, ((width + 3) // 4)) * max(1, ((height + 3) // 4)) * blockSize + + if dxgiFormat <= DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB: + name = dxgiFormat.name + "_" + count = 0 + currentNum = "" + for char in name: + if char.isdecimal(): + currentNum += char + elif currentNum != "": + count += int(currentNum) + currentNum = "" + pixelSize = (count + 7) // 8 + return width * height * pixelSize + pass diff --git a/libs/dds_header/src/DDS/DDSFile.py b/libs/dds_header/src/DDS/DDSFile.py new file mode 100644 index 0000000..4592074 --- /dev/null +++ b/libs/dds_header/src/DDS/DDSFile.py @@ -0,0 +1,220 @@ +import io + +from PyQt6.QtCore import QCoreApplication, qCritical, QFile, QIODeviceBase +from PyQt6.QtOpenGL import QOpenGLTexture + +from . import DDSDefinitions +from .glstuff import GLTextureFormat + + +class DDSReadException(Exception): + """Thrown if there was an error reading a DDS file""" + pass + + +ddsCubemapFaces = { + DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP_POSITIVEX: QOpenGLTexture.CubeMapFace.CubeMapPositiveX, + DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP_NEGATIVEX: QOpenGLTexture.CubeMapFace.CubeMapNegativeX, + DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP_POSITIVEY: QOpenGLTexture.CubeMapFace.CubeMapPositiveY, + DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP_NEGATIVEY: QOpenGLTexture.CubeMapFace.CubeMapNegativeY, + DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP_POSITIVEZ: QOpenGLTexture.CubeMapFace.CubeMapPositiveZ, + DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP_NEGATIVEZ: QOpenGLTexture.CubeMapFace.CubeMapNegativeZ} + + +class DDSFile: + def __init__(self, fileData: bytes, fileName: str): + self.fileName = fileName + self.header = DDSDefinitions.DDS_HEADER() + self.dxt10Header = None + self.glFormat: GLTextureFormat = None + self.fileData = fileData + self.data = None + self.isCubemap = None + + @classmethod + def fromFile(cls, fileName: str): + file = QFile(fileName) + if file.open(QIODeviceBase.OpenModeFlag.ReadOnly): + fileData = file.readAll() + else: + raise DDSReadException() + return cls(fileData.data(), fileName) + + def load(self): + with io.BytesIO(self.fileData) as file: + magicNumber = file.read(4) + if magicNumber != DDSDefinitions.DDS_MAGIC_NUMBER: + qCritical(self.tr("Magic number mismatch.")) + raise DDSReadException() + + self.header.fromStream(file) + + if self.header.ddspf.dwFlags & DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_FOURCC: + fourCC = self.header.ddspf.dwFourCC + if fourCC == b"DX10": + self.dxt10Header = DDSDefinitions.DDS_HEADER_DXT10() + self.dxt10Header.fromStream(file) + else: + fourCC = None + + self.glFormat = DDSDefinitions.getGLFormat(self.header.ddspf, self.dxt10Header) + self.data = [] + # Do this once per layer/mip level whatever, (times one per scanline if uncompressed). Also, potentially recompute this based on the format and size in case writers lie. + # self.data.append(file.read(self.header.dwPitchOrLinearSize)) + + layerCount = 1 + if self.header.dwCaps2 & DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP: + self.isCubemap = True + layerCount = 0 + for face in ddsCubemapFaces: + if self.header.dwCaps2 & face: + layerCount += 1 + else: + self.isCubemap = False + + for layer in range(layerCount): + nextWidth = self.header.dwWidth + nextHeight = self.header.dwHeight + mipCount = self.mipLevels() + for level in range(mipCount): + if self.header.ddspf.dwFlags & ( + DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_ALPHA | DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_RGB | DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_YUV | DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_LUMINANCE): + size = nextWidth * nextHeight * ((self.header.ddspf.dwRGBBitCount + 7) // 8) + elif fourCC: + if self.dxt10Header: + dxgiFormat = self.dxt10Header.dxgiFormat + else: + dxgiFormat = DDSDefinitions.fourCCToDXGI(fourCC) + size = DDSDefinitions.sizeFromFormat(dxgiFormat, nextWidth, nextHeight) + self.data.append(file.read(size)) + nextWidth = max(nextWidth // 2, 1) + nextHeight = max(nextHeight // 2, 1) + + def getDescription(self): + format = "" + # DX10 header says the format enum + if self.dxt10Header is not None: + format = self.dxt10Header.dxgiFormat.name.replace("DXGI_FORMAT_", "") + # Pixel Format says the FourCC + elif self.header.ddspf.dwFlags & DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_FOURCC: + fourCC = self.header.ddspf.dwFourCC + format = self.tr("{0} (equivalent to {1})").format(fourCC.decode('ascii'), + DDSDefinitions.fourCCToDXGI(fourCC).name.replace( + "DXGI_FORMAT_", "")) + # We've got bitmasks for the colour channels + else: + # This could be prettier if there was logic to detect that certain common bitmasks represented things more easily represented, like RGBA8 + if self.header.ddspf.dwFlags & ( + DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_RGB | DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_YUV): + format += self.tr("Red bitmask {0}, Green bitmask {1}, Blue bitmask {2}").format( + self.header.ddspf.dwRBitMask.hex().upper(), self.header.ddspf.dwGBitMask.hex().upper(), + self.header.ddspf.dwBBitMask.hex().upper()) + if self.header.ddspf.dwFlags & DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_LUMINANCE: + if format != "": + format += ", " + format += self.tr("Luminance bitmask {0}").format(self.header.ddspf.dwRBitMask.hex().upper()) + if self.header.ddspf.dwFlags & ( + DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_ALPHA | DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_ALPHAPIXELS): + if format != "": + format += ", " + format += self.tr("Alpha bitmask {0}").format(self.header.ddspf.dwABitMask.hex().upper()) + + size = self.tr("{0}×{1}").format(self.header.dwWidth, self.header.dwHeight) + + dimensions = self.tr("Cubemap") if self.isCubemap else self.tr("2D") + + mipmaps = self.tr("Mipmapped") if self.mipLevels() != 1 else self.tr("No mipmaps") + + return self.tr("{0}, {1} {2}, {3}").format(format, size, dimensions, mipmaps) + + def mipLevels(self): + if self.header.dwFlags & DDSDefinitions.DDS_HEADER.Flags.DDSD_MIPMAPCOUNT: + return self.header.dwMipMapCount + else: + return 1 + + def asQOpenGLTexture(self, gl, context): + if not self.data: + return + + if self.glFormat.requirements: + minVersion, extensions = self.glFormat.requirements + glVersion = (gl.glGetIntegerv(gl.GL_MAJOR_VERSION), gl.glGetIntegerv(gl.GL_MINOR_VERSION)) + if glVersion < minVersion or minVersion < (1, 0): + compatible = False + for extension in extensions: + if context.hasExtension(extension): + compatible = True + break + if not compatible: + qCritical(self.tr("OpenGL driver incompatible with texture format.")) + return None + + if self.header.dwCaps2 & DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP: + texture = QOpenGLTexture(QOpenGLTexture.Target.TargetCubeMap) + if self.header.dwWidth != self.header.dwHeight: + qCritical(self.tr("Cubemap faces must be square")) + return None + else: + # Assume GL_TEXTURE_2D for now + texture = QOpenGLTexture(QOpenGLTexture.Target.Target2D) + # Assume single layer for now + # self.texture.setLayers(1) + mipCount = self.mipLevels() + texture.setAutoMipMapGenerationEnabled(False) + texture.setMipLevels(mipCount) + texture.setMipLevelRange(0, mipCount - 1) + texture.setSize(self.header.dwWidth, self.header.dwHeight) + texture.setFormat(QOpenGLTexture.TextureFormat(self.glFormat.internalFormat)) + texture.allocateStorage() + + if self.header.dwCaps2 & DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP: + # Lisa hasn't whipped David Wang into shape yet. At least there are fewer bugs than under Raja. + # The specific bug has been reported and AMD "will try to reproduce it soon" + # MO 2.5.0: Radeon-specific code is causing crashing on the latest drivers + # Some cubemaps fail to render with or without these modifications + # noDSA = "Radeon" in gl.glGetString(gl.GL_RENDERER) and self.glFormat.compressed + noDSA = False + if noDSA: + texture.bind() + faceIndex = 0 + for face in ddsCubemapFaces: + if self.header.dwCaps2 & face: + for i in range(mipCount): + if self.glFormat.compressed: + if not noDSA: + texture.setCompressedData(i, 0, ddsCubemapFaces[face], + len(self.data[faceIndex * mipCount + i]), + self.data[faceIndex * mipCount + i]) + else: + gl.glCompressedTexSubImage2D(ddsCubemapFaces[face], i, 0, 0, + max(self.header.dwWidth // 2 ** i, 1), + max(self.header.dwHeight // 2 ** i, 1), + self.glFormat.internalFormat, + len(self.data[faceIndex * mipCount + i]), + self.data[faceIndex * mipCount + i]) + else: + texture.setData(i, 0, ddsCubemapFaces[face], QOpenGLTexture.PixelFormat(self.glFormat.format), QOpenGLTexture.PixelType(self.glFormat.type), + self.glFormat.converter(self.data[faceIndex * mipCount + i])) + faceIndex += 1 + if noDSA: + texture.release() + else: + for i in range(mipCount): + if self.glFormat.compressed: + texture.setCompressedData(i, 0, len(self.data[i]), self.data[i]) + else: + + texture.setData(i, 0, QOpenGLTexture.PixelFormat(self.glFormat.format), QOpenGLTexture.PixelType(self.glFormat.type), + self.glFormat.converter(self.data[i])) + + texture.setWrapMode(QOpenGLTexture.WrapMode.ClampToEdge) + + if self.glFormat.samplerType != "F": + # integer textures can't be filtered + texture.setMinMagFilters(QOpenGLTexture.Filter.NearestMipMapNearest, QOpenGLTexture.Filter.Nearest) + + return texture + + def tr(self, str): + return QCoreApplication.translate("DDSFile", str) diff --git a/libs/dds_header/src/DDS/__init__.py b/libs/dds_header/src/DDS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/dds_header/src/DDS/glstuff.py b/libs/dds_header/src/DDS/glstuff.py new file mode 100644 index 0000000..314d48f --- /dev/null +++ b/libs/dds_header/src/DDS/glstuff.py @@ -0,0 +1,205 @@ +from enum import IntEnum + + +class GL_IMAGE_FORMAT(IntEnum): + GL_BYTE = 0x1400 + GL_UNSIGNED_BYTE = 0x1401 + GL_SHORT = 0x1402 + GL_UNSIGNED_SHORT = 0x1403 + GL_INT = 0x1404 + GL_UNSIGNED_INT = 0x1405 + GL_FLOAT = 0x1406 + GL_HALF_FLOAT = 0x140B + GL_COLOR_INDEX = 0x1900 + GL_STENCIL_INDEX = 0x1901 + GL_DEPTH_COMPONENT = 0x1902 + GL_RED = 0x1903 + GL_GREEN = 0x1904 + GL_BLUE = 0x1905 + GL_ALPHA = 0x1906 + GL_RGB = 0x1907 + GL_RGBA = 0x1908 + GL_LUMINANCE = 0x1909 + GL_LUMINANCE_ALPHA = 0x190A + GL_BITMAP = 0x1A00 + GL_R3_G3_B2 = 0x2A10 + GL_UNSIGNED_BYTE_3_3_2 = 0x8032 + GL_UNSIGNED_SHORT_4_4_4_4 = 0x8033 + GL_UNSIGNED_SHORT_5_5_5_1 = 0x8034 + GL_UNSIGNED_INT_8_8_8_8 = 0x8035 + GL_UNSIGNED_INT_10_10_10_2 = 0x8036 + GL_ALPHA4 = 0x803B + GL_ALPHA8 = 0x803C + GL_ALPHA12 = 0x803D + GL_ALPHA16 = 0x803E + GL_LUMINANCE4 = 0x803F + GL_LUMINANCE8 = 0x8040 + GL_LUMINANCE12 = 0x8041 + GL_LUMINANCE16 = 0x8042 + GL_LUMINANCE4_ALPHA4 = 0x8043 + GL_LUMINANCE6_ALPHA2 = 0x8044 + GL_LUMINANCE8_ALPHA8 = 0x8045 + GL_LUMINANCE12_ALPHA4 = 0x8046 + GL_LUMINANCE12_ALPHA12 = 0x8047 + GL_LUMINANCE16_ALPHA16 = 0x8048 + GL_INTENSITY = 0x8049 + GL_INTENSITY4 = 0x804A + GL_INTENSITY8 = 0x804B + GL_INTENSITY12 = 0x804C + GL_INTENSITY16 = 0x804D + GL_RGB4 = 0x804F + GL_RGB5 = 0x8050 + GL_RGB8 = 0x8051 + GL_RGB10 = 0x8052 + GL_RGB12 = 0x8053 + GL_RGB16 = 0x8054 + GL_RGBA2 = 0x8055 + GL_RGBA4 = 0x8056 + GL_RGB5_A1 = 0x8057 + GL_RGBA8 = 0x8058 + GL_RGB10_A2 = 0x8059 + GL_RGBA12 = 0x805A + GL_RGBA16 = 0x805B + GL_BGR = 0x80E0 + GL_BGRA = 0x80E1 + GL_DEPTH_COMPONENT16 = 0x81A5 + GL_DEPTH_COMPONENT24 = 0x81A6 + GL_DEPTH_COMPONENT32 = 0x81A7 + GL_COMPRESSED_RED = 0x8225 + GL_COMPRESSED_RG = 0x8226 + GL_RG = 0x8227 + GL_RG_INTEGER = 0x8228 + GL_R8 = 0x8229 + GL_R16 = 0x822A + GL_RG8 = 0x822B + GL_RG16 = 0x822C + GL_R16F = 0x822D + GL_R32F = 0x822E + GL_RG16F = 0x822F + GL_RG32F = 0x8230 + GL_R8I = 0x8231 + GL_R8UI = 0x8232 + GL_R16I = 0x8233 + GL_R16UI = 0x8234 + GL_R32I = 0x8235 + GL_R32UI = 0x8236 + GL_RG8I = 0x8237 + GL_RG8UI = 0x8238 + GL_RG16I = 0x8239 + GL_RG16UI = 0x823A + GL_RG32I = 0x823B + GL_RG32UI = 0x823C + GL_UNSIGNED_BYTE_2_3_3_REV = 0x8362 + GL_UNSIGNED_SHORT_5_6_5 = 0x8363 + GL_UNSIGNED_SHORT_5_6_5_REV = 0x8364 + GL_UNSIGNED_SHORT_4_4_4_4_REV = 0x8365 + GL_UNSIGNED_SHORT_1_5_5_5_REV = 0x8366 + GL_UNSIGNED_INT_8_8_8_8_REV = 0x8367 + GL_UNSIGNED_INT_2_10_10_10_REV = 0x8368 + GL_COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0 + GL_COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1 + GL_COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2 + GL_COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3 + GL_COMPRESSED_ALPHA = 0x84E9 + GL_COMPRESSED_LUMINANCE = 0x84EA + GL_COMPRESSED_LUMINANCE_ALPHA = 0x84EB + GL_COMPRESSED_INTENSITY = 0x84EC + GL_COMPRESSED_RGB = 0x84ED + GL_COMPRESSED_RGBA = 0x84EE + GL_DEPTH_STENCIL = 0x84F9 + GL_RGBA32F = 0x8814 + GL_RGB32F = 0x8815 + GL_RGBA16F = 0x881A + GL_RGB16F = 0x881B + GL_R11F_G11F_B10F = 0x8C3A + GL_UNSIGNED_INT_10F_11F_11F_REV_EXT = 0x8C3B # EXT_packed_float + GL_RGB9_E5 = 0x8C3D + GL_UNSIGNED_INT_5_9_9_9_REV_EXT = 0x8C3E # EXT_texture_shared_exponent + GL_SRGB = 0x8C40 + GL_SRGB8 = 0x8C41 + GL_SRGB_ALPHA = 0x8C42 + GL_SRGB8_ALPHA8 = 0x8C43 + GL_SLUMINANCE_ALPHA = 0x8C44 + GL_SLUMINANCE8_ALPHA8 = 0x8C45 + GL_SLUMINANCE = 0x8C46 + GL_SLUMINANCE8 = 0x8C47 + GL_COMPRESSED_SRGB = 0x8C48 + GL_COMPRESSED_SRGB_ALPHA = 0x8C49 + GL_COMPRESSED_SRGB_S3TC_DXT1_EXT = 0x8C4C + GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT = 0x8C4D + GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT = 0x8C4E + GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT = 0x8C4F + GL_RGBA32UI = 0x8D70 + GL_RGB32UI = 0x8D71 + GL_RGBA16UI = 0x8D76 + GL_RGB16UI = 0x8D77 + GL_RGBA8UI = 0x8D7C + GL_RGB8UI = 0x8D7D + GL_RGBA32I = 0x8D82 + GL_RGB32I = 0x8D83 + GL_RGBA16I = 0x8D88 + GL_RGB16I = 0x8D89 + GL_RGBA8I = 0x8D8E + GL_RGB8I = 0x8D8F + GL_RED_INTEGER = 0x8D94 + GL_RGB_INTEGER = 0x8D98 + GL_RGBA_INTEGER = 0x8D99 + GL_BGR_INTEGER = 0x8D9A + GL_BGRA_INTEGER = 0x8D9B + GL_COMPRESSED_RED_RGTC1 = 0x8DBB + GL_COMPRESSED_SIGNED_RED_RGTC1 = 0x8DBC + GL_COMPRESSED_RG_RGTC2 = 0x8DBD + GL_COMPRESSED_SIGNED_RG_RGTC2 = 0x8DBE + GL_COMPRESSED_RGBA_BPTC_UNORM = 0x8E8C + GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM = 0x8E8D + GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT = 0x8E8E + GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT = 0x8E8F + GL_R8_SNORM = 0x8F94 + GL_RG8_SNORM = 0x8F95 + GL_RGB8_SNORM = 0x8F96 + GL_RGBA8_SNORM = 0x8F97 + GL_R16_SNORM = 0x8F98 + GL_RG16_SNORM = 0x8F99 + GL_RGB16_SNORM = 0x8F9A + GL_RGBA16_SNORM = 0x8F9B + GL_RGB10_A2UI = 0x906F + GL_COMPRESSED_R11_EAC = 0x9270 + GL_COMPRESSED_SIGNED_R11_EAC = 0x9271 + GL_COMPRESSED_RG11_EAC = 0x9272 + GL_COMPRESSED_SIGNED_RG11_EAC = 0x9273 + GL_COMPRESSED_RGB8_ETC2 = 0x9274 + GL_COMPRESSED_SRGB8_ETC2 = 0x9275 + GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276 + GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277 + GL_COMPRESSED_RGBA8_ETC2_EAC = 0x9278 + GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279 + + +class GLTextureFormat: + def __init__(self, requirements, internalFormat, compressed): + self.requirements = requirements + self.internalFormat = internalFormat + self.compressed = compressed + + if internalFormat.name.endswith("UI"): + self.samplerType = "UI" + elif internalFormat.name.endswith("I"): + self.samplerType = "I" + else: + self.samplerType = "F" + + +class CompressedGLTextureFormat(GLTextureFormat): + def __init__(self, requirements, internalFormat): + super().__init__(requirements, internalFormat, True) + + +class UncompressedGLTextureFormat(GLTextureFormat): + def __init__(self, requirements, internalFormat, format, type, converter=None): + super().__init__(requirements, internalFormat, False) + self.format = format + self.type = type + if converter: + self.converter = converter + else: + self.converter = lambda x: x diff --git a/libs/dds_header/src/DDSPreview.py b/libs/dds_header/src/DDSPreview.py new file mode 100644 index 0000000..78ffffc --- /dev/null +++ b/libs/dds_header/src/DDSPreview.py @@ -0,0 +1,528 @@ +import struct +import sys +import threading +import enum + +from PyQt6.QtCore import QCoreApplication, qDebug, Qt, QSize +from PyQt6.QtGui import QColor, QOpenGLContext, QSurfaceFormat, QMatrix4x4, QVector4D +from PyQt6.QtOpenGLWidgets import QOpenGLWidget +from PyQt6.QtWidgets import QGridLayout, QLabel, QPushButton, QWidget, QColorDialog, QComboBox +from PyQt6.QtOpenGL import QOpenGLBuffer, QOpenGLDebugLogger, QOpenGLShader, QOpenGLShaderProgram, QOpenGLTexture, \ + QOpenGLVersionProfile, QOpenGLVertexArrayObject, QOpenGLFunctions_4_1_Core, QOpenGLVersionFunctionsFactory + +from DDS.DDSFile import DDSFile + +if "mobase" not in sys.modules: + import mobase + +vertexShader2D = """ +#version 150 + +uniform float aspectRatioRatio; + +in vec4 position; +in vec2 texCoordIn; + +out vec2 texCoord; + +void main() +{ + texCoord = texCoordIn; + gl_Position = position; + if (aspectRatioRatio >= 1.0) + gl_Position.y /= aspectRatioRatio; + else + gl_Position.x *= aspectRatioRatio; +} +""" + +vertexShaderCube = """ +#version 150 + +uniform float aspectRatioRatio; + +in vec4 position; +in vec2 texCoordIn; + +out vec2 texCoord; + +void main() +{ + texCoord = texCoordIn; + gl_Position = position; +} +""" + +fragmentShaderFloat = """ +#version 150 + +uniform sampler2D aTexture; +uniform mat4 channelMatrix; +uniform vec4 channelOffset; + +in vec2 texCoord; + +void main() +{ + gl_FragData[0] = channelMatrix * texture(aTexture, texCoord) + channelOffset; +} +""" + +fragmentShaderUInt = """ +#version 150 + +uniform usampler2D aTexture; +uniform mat4 channelMatrix; +uniform vec4 channelOffset; + +in vec2 texCoord; + +void main() +{ + // autofilled alpha is 1, so if we have a scaling factor, we need separate ones for luminance and alpha + gl_FragData[0] = channelMatrix * texture(aTexture, texCoord) + channelOffset; +} +""" + +fragmentShaderSInt = """ +#version 150 + +uniform isampler2D aTexture; +uniform mat4 channelMatrix; +uniform vec4 channelOffset; + +in vec2 texCoord; + +void main() +{ + // autofilled alpha is 1, so if we have a scaling factor and offset, we need separate ones for luminance and alpha + gl_FragData[0] = channelMatrix * texture(aTexture, texCoord) + channelOffset; +} +""" + +fragmentShaderCube = """ +#version 150 + +uniform samplerCube aTexture; +uniform mat4 channelMatrix; +uniform vec4 channelOffset; + +in vec2 texCoord; + +const float PI = 3.1415926535897932384626433832795; + +void main() +{ + float theta = -2.0 * PI * texCoord.x; + float phi = PI * texCoord.y; + gl_FragData[0] = channelMatrix * texture(aTexture, vec3(sin(theta) * sin(phi), cos(theta) * sin(phi), cos(phi))) + channelOffset; +} +""" + +transparencyVS = """ +#version 150 + +in vec4 position; + +void main() +{ + gl_Position = position; +} +""" + +transparencyFS = """ +#version 150 + +uniform vec4 backgroundColour; + +void main() +{ + float x = gl_FragCoord.x; + float y = gl_FragCoord.y; + x = mod(x, 16.0); + y = mod(y, 16.0); + gl_FragData[0] = x < 8.0 ^^ y < 8.0 ? vec4(vec3(191.0/255.0), 1.0) : vec4(1.0); + gl_FragData[0].rgb = backgroundColour.rgb * backgroundColour.a + gl_FragData[0].rgb * (1.0 - backgroundColour.a); +} +""" + +vertices = [ + # vertex coordinates texture coordinates + -1.0, -1.0, 0.5, 1.0, 0.0, 1.0, + -1.0, 1.0, 0.5, 1.0, 0.0, 0.0, + 1.0, 1.0, 0.5, 1.0, 1.0, 0.0, + + -1.0, -1.0, 0.5, 1.0, 0.0, 1.0, + 1.0, 1.0, 0.5, 1.0, 1.0, 0.0, + 1.0, -1.0, 0.5, 1.0, 1.0, 1.0, +] + + +class DDSOptions: + def __init__(self, colour: QColor = QColor(0, 0, 0, 0), channelMatrix: QMatrix4x4 = QMatrix4x4(), + channelOffset: QVector4D = QVector4D()): + # QMatrix4x4 with no arguments is the identity matrix, so no channel transformations + # declare member variables with None values + self.backgroundColour = None + self.channelMatrix = None + self.channelOffset = None + # initialize member variables with error checks + self.setBackgroundColour(colour) + self.setChannelMatrix(channelMatrix) + self.setChannelOffset(channelOffset) + + def setBackgroundColour(self, colour: QColor): + if isinstance(colour, QColor) and colour.isValid(): + self.backgroundColour = colour + else: + raise TypeError(str(colour) + " is not a valid QColor object.") + + def getBackgroundColour(self) -> QColor: + return self.backgroundColour + + def getChannelMatrix(self) -> QMatrix4x4: + return self.channelMatrix + + def setChannelMatrix(self, matrix): + self.channelMatrix = QMatrix4x4(matrix) + + def getChannelOffset(self) -> QVector4D: + return self.channelOffset + + def setChannelOffset(self, vector): + self.channelOffset = QVector4D(vector) + + +glVersionProfile = QOpenGLVersionProfile() +glVersionProfile.setVersion(2, 1) + + +class DDSWidget(QOpenGLWidget): + def __init__(self, ddsFile, ddsOptions=DDSOptions(), debugContext=False, parent=None, f=Qt.WindowType(0)): + super(DDSWidget, self).__init__(parent, f) + + self.ddsFile = ddsFile + + self.ddsOptions = ddsOptions + + self.clean = True + + self.logger = None + + self.program = None + self.transparecyProgram = None + self.texture = None + self.vbo = None + self.vao = None + + if debugContext: + format = QSurfaceFormat() + format.setOption(QSurfaceFormat.FormatOption.DebugContext) + self.setFormat(format) + self.logger = QOpenGLDebugLogger(self) + + def __del__(self): + self.cleanup() + + def __dtor__(self): + self.cleanup() + + def initializeGL(self): + if self.logger: + self.logger.initialize() + self.logger.messageLogged.connect( + lambda message: qDebug(self.tr("OpenGL debug message: {0}").format(message.message()))) + self.logger.startLogging() + + gl = QOpenGLVersionFunctionsFactory.get(glVersionProfile) + QOpenGLContext.currentContext().aboutToBeDestroyed.connect(self.cleanup) + + self.clean = False + + fragmentShader = None + vertexShader = vertexShader2D + if self.ddsFile.isCubemap: + fragmentShader = fragmentShaderCube + vertexShader = vertexShaderCube + if QOpenGLContext.currentContext().hasExtension(b"GL_ARB_seamless_cube_map"): + GL_TEXTURE_CUBE_MAP_SEAMLESS = 0x884F + gl.glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS) + elif self.ddsFile.glFormat.samplerType == "F": + fragmentShader = fragmentShaderFloat + elif self.ddsFile.glFormat.samplerType == "UI": + fragmentShader = fragmentShaderUInt + else: + fragmentShader = fragmentShaderSInt + + self.program = QOpenGLShaderProgram(self) + self.program.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Vertex, vertexShader) + self.program.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Fragment, fragmentShader) + self.program.bindAttributeLocation("position", 0) + self.program.bindAttributeLocation("texCoordIn", 1) + self.program.link() + + self.transparecyProgram = QOpenGLShaderProgram(self) + self.transparecyProgram.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Vertex, transparencyVS) + self.transparecyProgram.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Fragment, transparencyFS) + self.transparecyProgram.bindAttributeLocation("position", 0) + self.transparecyProgram.link() + + self.vao = QOpenGLVertexArrayObject(self) + vaoBinder = QOpenGLVertexArrayObject.Binder(self.vao) + + self.vbo = QOpenGLBuffer(QOpenGLBuffer.Type.VertexBuffer) + self.vbo.create() + self.vbo.bind() + + theBytes = struct.pack("%sf" % len(vertices), *vertices) + self.vbo.allocate(theBytes, len(theBytes)) + + gl.glEnableVertexAttribArray(0) + gl.glEnableVertexAttribArray(1) + gl.glVertexAttribPointer(0, 4, gl.GL_FLOAT, False, 6 * 4, 0) + gl.glVertexAttribPointer(1, 2, gl.GL_FLOAT, False, 6 * 4, 4 * 4) + + self.texture = self.ddsFile.asQOpenGLTexture(gl, QOpenGLContext.currentContext()) + + def resizeGL(self, w, h): + aspectRatioTex = self.texture.width() / self.texture.height() if self.texture else 1.0 + aspectRatioWidget = w / h + ratioRatio = aspectRatioTex / aspectRatioWidget + + self.program.bind() + self.program.setUniformValue("aspectRatioRatio", ratioRatio) + self.program.release() + + def paintGL(self): + gl = QOpenGLVersionFunctionsFactory.get(glVersionProfile) + + vaoBinder = QOpenGLVertexArrayObject.Binder(self.vao) + + # Draw checkerboard so transparency is obvious + self.transparecyProgram.bind() + + backgroundColour = self.ddsOptions.getBackgroundColour() + if backgroundColour and backgroundColour.isValid(): + self.transparecyProgram.setUniformValue("backgroundColour", backgroundColour) + + gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6) + + self.transparecyProgram.release() + + self.program.bind() + + if self.texture: + self.texture.bind() + + gl.glEnable(gl.GL_BLEND) + gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) + + self.program.setUniformValue("channelMatrix", self.ddsOptions.getChannelMatrix()) + self.program.setUniformValue("channelOffset", self.ddsOptions.getChannelOffset()) + + gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6) + + if self.texture: + self.texture.release() + self.program.release() + + def cleanup(self): + if not self.clean: + self.makeCurrent() + + self.program = None + self.transparecyProgram = None + if self.texture: + self.texture.destroy() + self.texture = None + self.vbo.destroy() + self.vbo = None + self.vao.destroy() + self.vao = None + + self.doneCurrent() + self.clean = True + + def tr(self, str): + return QCoreApplication.translate("DDSWidget", str) + + +class ColourChannels(enum.Enum): + RGBA = "Colour and Alpha" + RGB = "Colour" + A = "Alpha" + R = "Red" + G = "Green" + B = "Blue" + + +class DDSChannelManager: + def __init__(self, channels: ColourChannels): + self.channels = channels + + def setChannels(self, options: DDSOptions, channels: ColourChannels): + self.channels = channels + + def drawColour(alpha: bool): + colorMatrix = QMatrix4x4() + colorOffset = QVector4D() + if not alpha: + colorMatrix[3, 3] = 0 + colorOffset.setW(1.0) + options.setChannelMatrix(colorMatrix) + options.setChannelOffset(colorOffset) + + def drawGrayscale(channel: ColourChannels): + colorOffset = QVector4D(0, 0, 0, 1) + channelVector = [0, 0, 0, 0] + if channels == ColourChannels.R: + channelVector[0] = 1 + elif channel == ColourChannels.G: + channelVector[1] = 1 + elif channel == ColourChannels.B: + channelVector[2] = 1 + elif channels == ColourChannels.A: + channelVector[3] = 1 + else: + raise ValueError("channel must be a single color channel.") + alphaVector = [0, 0, 0, 0] + colorMatrix = channelVector * 3 + alphaVector + options.setChannelMatrix(colorMatrix) + options.setChannelOffset(colorOffset) + + if channels == ColourChannels.RGBA: + drawColour(True) + elif channels == ColourChannels.RGB: + drawColour(False) + else: + drawGrayscale(channels) + + +class DDSPreview(mobase.IPluginPreview): + + def __init__(self): + super().__init__() + self.__organizer = None + self.options = None + self.channelManager = None + + def init(self, organizer: mobase.IOrganizer): + self.__organizer = organizer + savedColour = QColor(self.pluginSetting("background r"), self.pluginSetting("background g"), + self.pluginSetting("background b"), self.pluginSetting("background a")) + try: + savedChannels = ColourChannels[self.pluginSetting("channels")] + except KeyError: + savedChannels = ColourChannels.RGBA + self.options = DDSOptions(savedColour) + self.channelManager = DDSChannelManager(savedChannels) + self.channelManager.setChannels(self.options, savedChannels) + return True + + def pluginSetting(self, name): + return self.__organizer.pluginSetting(self.name(), name) + + def setPluginSetting(self, name, value): + self.__organizer.setPluginSetting(self.name(), name, value) + + def name(self): + return "DDS Preview Plugin" + + def author(self): + return "AnyOldName3" + + def description(self): + return self.tr("Lets you preview DDS files by actually uploading them to the GPU.") + + def version(self): + return mobase.VersionInfo(1, 0, 1, 0) + + def settings(self): + return [mobase.PluginSetting("log gl errors", self.tr( + "If enabled, log OpenGL errors and debug messages. May decrease performance."), False), + mobase.PluginSetting("background r", self.tr("Red channel of background colour"), 0), + mobase.PluginSetting("background g", self.tr("Green channel of background colour"), 0), + mobase.PluginSetting("background b", self.tr("Blue channel of background colour"), 0), + mobase.PluginSetting("background a", self.tr("Alpha channel of background colour"), 0), + mobase.PluginSetting("channels", self.tr("The colour channels that are displayed."), + ColourChannels.RGBA.name)] + + def supportedExtensions(self) -> set[str]: + return {"dds"} + + def supportsArchives(self) -> bool: + return True + + def genFilePreview(self, fileName: str, maxSize: QSize) -> QWidget: + return self.previewFromDDSFile(DDSFile.fromFile(fileName)) + + def genDataPreview(self, fileData: bytes, fileName: str, maxSize: QSize) -> QWidget: + return self.previewFromDDSFile(DDSFile(fileData, fileName)) + + def previewFromDDSFile(self, ddsFile: DDSFile) -> QWidget: + ddsFile.load() + layout = QGridLayout() + # Image grows before label and button + layout.setRowStretch(0, 1) + # Label grows before button + layout.setColumnStretch(0, 1) + layout.addWidget(self.__makeLabel(ddsFile), 1, 0, 1, 1) + + ddsWidget = DDSWidget(ddsFile, self.options, self.__organizer.pluginSetting(self.name(), "log gl errors")) + layout.addWidget(ddsWidget, 0, 0, 1, 3) + + layout.addWidget(self.__makeColourButton(ddsWidget), 1, 2, 1, 1) + layout.addWidget(self.__makeChannelsButton(ddsWidget), 1, 1, 1, 1) + + widget = QWidget() + widget.setLayout(layout) + return widget + + def tr(self, str): + return QCoreApplication.translate("DDSPreview", str) + + def __makeLabel(self, ddsFile): + label = QLabel(ddsFile.getDescription()) + label.setWordWrap(True) + label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + return label + + def __makeColourButton(self, ddsWidget): + button = QPushButton(self.tr("Pick background colour")) + + def pickColour(unused): + newColour = QColorDialog.getColor(self.options.getBackgroundColour(), button, "Background colour", + QColorDialog.ColorDialogOption.ShowAlphaChannel) + if newColour.isValid(): + self.setPluginSetting("background r", newColour.red()) + self.setPluginSetting("background g", newColour.green()) + self.setPluginSetting("background b", newColour.blue()) + self.setPluginSetting("background a", newColour.alpha()) + self.options.setBackgroundColour(newColour) + ddsWidget.update() + + button.clicked.connect(pickColour) + return button + + def __makeChannelsButton(self, ddsWidget): + listwidget = QComboBox() + channelKeys = [e.name for e in ColourChannels] + channelNames = [e.value for e in ColourChannels] + + listwidget.addItems(channelNames) + listwidget.setCurrentText(self.channelManager.channels.value) + listwidget.setToolTip(self.tr("Select what colour channels are displayed.")) + + listwidget.showEvent = lambda _: listwidget.setCurrentText(self.channelManager.channels.value) + + def onChanged(newIndex): + self.channelManager.setChannels(self.options, ColourChannels[channelKeys[newIndex]]) + self.setPluginSetting("channels", self.channelManager.channels.name) + ddsWidget.update() + + listwidget.currentIndexChanged.connect(onChanged) + return listwidget + + +def createPlugin(): + return DDSPreview() diff --git a/libs/dds_header/src/DDSPreview_en.ts b/libs/dds_header/src/DDSPreview_en.ts new file mode 100644 index 0000000..bc6308a --- /dev/null +++ b/libs/dds_header/src/DDSPreview_en.ts @@ -0,0 +1,128 @@ + + + + + DDSFile + + + Magic number mismatch. + + + + + {0} (equivalent to {1}) + + + + + Red bitmask {0}, Green bitmask {1}, Blue bitmask {2} + + + + + Luminance bitmask {0} + + + + + Alpha bitmask {0} + + + + + {0}×{1} + + + + + Cubemap + + + + + 2D + + + + + Mipmapped + + + + + No mipmaps + + + + + {0}, {1} {2}, {3} + + + + + OpenGL driver incompatible with texture format. + + + + + Cubemap faces must be square + + + + + DDSPreview + + + Lets you preview DDS files by actually uploading them to the GPU. + + + + + If enabled, log OpenGL errors and debug messages. May decrease performance. + + + + + Red channel of background colour + + + + + Green channel of background colour + + + + + Blue channel of background colour + + + + + Alpha channel of background colour + + + + + The colour channels that are displayed. + + + + + Pick background colour + + + + + Select what colour channels are displayed. + + + + + DDSWidget + + + OpenGL debug message: {0} + + + + diff --git a/libs/dds_header/vcpkg.json b/libs/dds_header/vcpkg.json new file mode 100644 index 0000000..da01ba4 --- /dev/null +++ b/libs/dds_header/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "27d8adbfe9e4ce88a875be3a45fadab69869eb60" + } + } +} diff --git a/libs/diagnose_basic/.clang-format b/libs/diagnose_basic/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/diagnose_basic/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/diagnose_basic/.gitattributes b/libs/diagnose_basic/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/diagnose_basic/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/diagnose_basic/.github/workflows/build.yml b/libs/diagnose_basic/.github/workflows/build.yml new file mode 100644 index 0000000..2fbd625 --- /dev/null +++ b/libs/diagnose_basic/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build Diagnose Basic + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Diagnose Basic + id: build-diagnose-basic + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase diff --git a/libs/diagnose_basic/.github/workflows/linting.yml b/libs/diagnose_basic/.github/workflows/linting.yml new file mode 100644 index 0000000..d91deb3 --- /dev/null +++ b/libs/diagnose_basic/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Diagnose Basic + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/diagnose_basic/.gitignore b/libs/diagnose_basic/.gitignore new file mode 100644 index 0000000..40c636d --- /dev/null +++ b/libs/diagnose_basic/.gitignore @@ -0,0 +1,6 @@ +CMakeLists.txt.user +edit +*.bak +/msbuild.log +/*std*.log +/*build diff --git a/libs/diagnose_basic/.pre-commit-config.yaml b/libs/diagnose_basic/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/diagnose_basic/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/diagnose_basic/CMakeLists.txt b/libs/diagnose_basic/CMakeLists.txt new file mode 100644 index 0000000..a5a3c62 --- /dev/null +++ b/libs/diagnose_basic/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(diagnose_basic) + +add_subdirectory(src) diff --git a/libs/diagnose_basic/CMakePresets.json b/libs/diagnose_basic/CMakePresets.json new file mode 100644 index 0000000..023baf5 --- /dev/null +++ b/libs/diagnose_basic/CMakePresets.json @@ -0,0 +1,52 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg"], + "name": "vs2022-windows", + "toolset": "v143" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/diagnose_basic/src/CMakeLists.txt b/libs/diagnose_basic/src/CMakeLists.txt new file mode 100644 index 0000000..86951b8 --- /dev/null +++ b/libs/diagnose_basic/src/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB diagnose_basic_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(diagnose_basic SHARED ${diagnose_basic_SOURCES}) +mo2_configure_plugin(diagnose_basic NO_SOURCES WARNINGS OFF) +target_link_libraries(diagnose_basic PRIVATE mo2::uibase) +mo2_install_plugin(diagnose_basic) diff --git a/libs/diagnose_basic/src/diagnose_basic_en.ts b/libs/diagnose_basic/src/diagnose_basic_en.ts new file mode 100644 index 0000000..cb5fb2f --- /dev/null +++ b/libs/diagnose_basic/src/diagnose_basic_en.ts @@ -0,0 +1,178 @@ + + + + + DiagnoseBasic + + + Checks for problems unrelated to other plugins + + + + + Warn when an error occurred last time an application was run + + + + + Warn when there are files in the overwrite directory + + + + + Warn when the font configuration refers to files that aren't installed + + + + + Warn when mods are installed that conflict with MO functionality + + + + + Warn when there are esps with missing masters + + + + + Warn when an installed mod came from an alternative game source + + + + + Warn when files have unwanted attributes + + + + + Ignore empty directories when checking overwrite directory + + + + + Ignore .log files and empty directories when checking overwrite directory + + + + + File attribute checker +Searching for problems... + + + + + Invalid file attributes found + + + + + One or more of your files has attributes that may prevent the game from reading the files. This can result in missing plugins, missing textures, and other such problems. + +Fix the file attributes? + + + + + File attribute checker +Fixing file attributes... + + + + + Unable to set file attributes + + + + + Mod Organizer was unable to fix the file attributes of at least one file. + +Continue launching %1? + + + + + There was an error reported recently + + + + + There are files in your Overwrite mod directory + There are files in your overwrite mod + + + + + Your font configuration may be broken + + + + + Nitpick installed + + + + + INI Tweaks overwritten + Ini Tweaks overwritten + + + + + Missing Masters + + + + + At least one unverified mod is using an alternative game source + + + + + + + invalid problem key %1 + + + + + There are currently files in your <span style="color: red;"><i>Overwrite</i></span> directory. These files are typically newly created files, usually generated by an external mod tool (i.e. Wrye Bash, xEdit, FNIS, ...). Creation Club ESL files will also end up here when downloaded. Any files in <span style="font-weight: bold;">Overwrite</span> will take top priority when loading your mod files and will always overwrite any other mod in your profile.<br><br>It is recommended that you review the files in <span style="font-weight: bold;">Overwrite</span> and move any relevant files to a new or existing mod. You can do this by double-clicking the <span style="font-weight: bold;">Overwrite</span> mod and dragging files from the Overwrite window to a mod entry in the main mod list. It is also possible to move all current <span style="font-weight: bold;">Overwrite</span> files to a new mod by right-clicking on the Overwrite mod.<br><br>Not all files in <span style="font-weight: bold;">Overwrite</span> need to be removed, but there are several reasons to do so. Some generated files will be directly related to the active mods in your profile and will be incompatible with different mod setups. Since <span style="font-weight: bold;">Overwrite</span> is always active, this could cause conflicts between profiles. Additionally, moving relevant game files into a normal mod will give you greater control over those files. Some files can live safely in <span style="font-weight: bold;">Overwrite</span>, such as basic logs and cache files. It is up to you to understand how best to manage these files.<br><br>If you do not wish to see this warning and understand how to handle your <span style="font-weight: bold;">Overwrite</span> directory, you can open the Mod Organizer settings and disable this warning under the "Diagnose Basic" plugin configuration. + There are currently files in your <font color="red"><i>Overwrite</i></font> directory. These files are typically newly created files, usually generated by an external mod tool (i.e. Wrye Bash, xEdit, FNIS, ...). Creation Club ESL files will also end up here when downloaded. Any files in <font color="red"><i>Overwrite</i></font> will take top priority when loading your mod files and will always overwrite any other mod in your profile.<br><br>It is recommended that you review the files in <font color="red"><i>Overwrite</i></font> and move any relevant files to a new or existing mod. You can do this by double-clicking the <font color="red"><i>Overwrite</i></font> mod and dragging files from the <font color="red"><i>Overwrite</i></font> window to a mod entry in the main mod list. It is also possible to move all current <font color="red"><i>Overwrite</i></font> files to a new mod by right-clicking on the <font color="red"><i>Overwrite</i></font> mod.<br><br>Not all files in <font color="red"><i>Overwrite</i></font> need to be removed, but there are several reasons to do so. Some generated files will be directly related to the active mods in your profile and will be incompatible with different mod setups. Since <font color="red"><i>Overwrite</i></font> is always active, this could cause conflicts between profiles. Additionally, moving relevant game files into a normal mod will give you greater control over those files. Some files can live safely in <font color="red"><i>Overwrite</i></font>, such as basic logs and cache files. It is up to you to understand how best to manage these files.<br><br>If you do not wish to see this warning and understand how to handle your <font color="red"><i>Overwrite</i></font> directory, you can open the Mod Organizer settings and disable this warning under the "Diagnose Basic" plugin configuration. + + + + + Your current configuration seems to reference a font that is not installed. You may see only boxes instead of letters.<br>The font configuration is in Data\interface\fontconfig.txt. Most likely you have a broken installation of a font replacer mod. + + + + + You have the nitpick skse plugin installed. This plugin is not needed with Mod Organizer because MO already offers the same functionality. Worse: The two solutions may conflict so it's strongly suggested you remove this plugin. + + + + + Settings provided in ini tweaks have been overwritten in-game or in an applications.<br>These overwrites are stored in a separate file (<i>profile_tweaks.ini</i> within the profile directory)<br>to keep ini-tweaks in their original state but you should really get rid of this file as there is<br>no tool support in MO to work on it. <br>Advice: Copy settings you want to keep to an appropriate ini tweak, then delete <i>profile_tweaks.ini</i>.<br>Hitting the <i>Fix</i> button will delete that file + + + + + The masters for some plugins (esp/esl/esm) are not enabled.<br>The game will crash unless you install and enable the following plugins: + + + + + Master + + + + + Required By + + + + + You have at least one active mod installed from an alternative game source.<br>This means that the mod was downloaded from a game source which does not match<br>the expected primary game.<br><br>Depending on the type of mod, this may require converting various files to run correctly.<br><br>Advice: Once you have verified the mod is working correctly, you can use the context menu<br>and select "Mark as converted/working" to remove the flag and warning. + + + + diff --git a/libs/diagnose_basic/src/diagnosebasic.cpp b/libs/diagnose_basic/src/diagnosebasic.cpp new file mode 100644 index 0000000..395a049 --- /dev/null +++ b/libs/diagnose_basic/src/diagnosebasic.cpp @@ -0,0 +1,803 @@ +/* + * Copyright (C) 2013 Sebastian Herbord. All rights reserved. + * + * This file is part of the basic diagnosis plugin for Mod Organizer + * + * This plugin is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This plugin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this plugin. If not, see . + */ + +#include "diagnosebasic.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace MOBase; + +const QRegularExpression DiagnoseBasic::RE_LOG_FILE(".*[.]log[0-9]*$"); +const unsigned int DiagnoseBasic::PROBLEM_ERRORLOG; +const unsigned int DiagnoseBasic::PROBLEM_OVERWRITE; +const unsigned int DiagnoseBasic::PROBLEM_INVALIDFONT; +const unsigned int DiagnoseBasic::PROBLEM_NITPICKINSTALLED; +const unsigned int DiagnoseBasic::PROBLEM_PROFILETWEAKS; +const unsigned int DiagnoseBasic::PROBLEM_MISSINGMASTERS; +const unsigned int DiagnoseBasic::PROBLEM_ALTERNATE; +const unsigned int DiagnoseBasic::NUM_CONTEXT_ROWS; + +DiagnoseBasic::DiagnoseBasic() : m_MOInfo(nullptr) {} + +bool DiagnoseBasic::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + + m_MOInfo->modList()->onModStateChanged( + [&](const std::map& mods) { + if (mods.contains("Overwrite")) + invalidate(); + }); + m_MOInfo->modList()->onModMoved([&](const QString&, int, int) { + // invalidates only the assetOrder check but there is currently no way to recheck + // individual checks + invalidate(); + }); + m_MOInfo->pluginList()->onPluginMoved([&](const QString&, int, int) { + invalidate(); + }); + m_MOInfo->pluginList()->onRefreshed([&]() { + invalidate(); + }); + m_MOInfo->pluginList()->onPluginStateChanged([&](auto const&) { + invalidate(); + }); + m_MOInfo->onAboutToRun([&](const QString& executable) { + return fileAttributes(executable); + }); + + return true; +} + +QString DiagnoseBasic::name() const +{ + return "Basic diagnosis plugin"; +} + +QString DiagnoseBasic::author() const +{ + return "Tannin"; +} + +QString DiagnoseBasic::description() const +{ + return tr("Checks for problems unrelated to other plugins"); +} + +VersionInfo DiagnoseBasic::version() const +{ + return VersionInfo(1, 1, 3, VersionInfo::RELEASE_FINAL); +} + +bool DiagnoseBasic::isActive() const +{ + return true; +} + +QList DiagnoseBasic::settings() const +{ + return QList() + << PluginSetting( + "check_errorlog", + tr("Warn when an error occurred last time an application was run"), + true) + << PluginSetting("check_overwrite", + tr("Warn when there are files in the overwrite directory"), + true) + << PluginSetting("check_font", + tr("Warn when the font configuration refers to files that " + "aren't installed"), + true) + << PluginSetting( + "check_conflict", + tr("Warn when mods are installed that conflict with MO functionality"), + true) + << PluginSetting("check_missingmasters", + tr("Warn when there are esps with missing masters"), true) + << PluginSetting( + "check_alternategames", + tr("Warn when an installed mod came from an alternative game source"), + false) + << PluginSetting("check_fileattributes", + tr("Warn when files have unwanted attributes"), false) + << PluginSetting( + "ow_ignore_empty", + tr("Ignore empty directories when checking overwrite directory"), false) + << PluginSetting("ow_ignore_log", + tr("Ignore .log files and empty directories when checking " + "overwrite directory"), + false); +} + +bool DiagnoseBasic::errorReported() const +{ + QDir dir(qApp->property("dataPath").toString() + "/logs"); + QFileInfoList files = + dir.entryInfoList(QStringList("ModOrganizer_??_??_??_??_??.log"), QDir::Files, + QDir::Name | QDir::Reversed); + + if (files.count() > 0) { + QString logFile = files.at(0).absoluteFilePath(); + QFile file(logFile); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + char buffer[1024]; + int line = 0; + qint64 lineLengths[NUM_CONTEXT_ROWS]; + for (int i = 0; i < NUM_CONTEXT_ROWS; ++i) { + lineLengths[i] = 0; + } + while (!file.atEnd()) { + lineLengths[line % NUM_CONTEXT_ROWS] = file.readLine(buffer, 1024) + 1; + if (strncmp(buffer, "ERROR", 5) == 0) { + qint64 sumChars = 0; + for (int i = 0; i < NUM_CONTEXT_ROWS; ++i) { + sumChars += lineLengths[i]; + } + file.seek(file.pos() - sumChars); + m_ErrorMessage = ""; + for (int i = 0; i < 2 * NUM_CONTEXT_ROWS; ++i) { + file.readLine(buffer, 1024); + QString lineString = QString::fromUtf8(buffer); + if (lineString.startsWith("ERROR")) { + m_ErrorMessage += "" + lineString + ""; + } else { + m_ErrorMessage += lineString; + } + } + return true; + } + + // prevent this function from taking forever + if (line++ >= 50000) { + break; + } + } + } + } + + return false; +} + +bool DiagnoseBasic::checkEmpty(QString const& path) const +{ + QDir dir(path); + dir.setFilter(QDir::Files | QDir::Hidden | QDir::System); + + // Search files first + for (auto const& file : dir.entryList()) { + if (!m_MOInfo->pluginSetting(name(), "ow_ignore_log").toBool() || + !RE_LOG_FILE.match(file).hasMatch()) { + return false; + } + } + + // Then directories + dir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); + for (QFileInfo const& subdir : dir.entryInfoList()) { + if (!checkEmpty(subdir.absoluteFilePath())) { + return false; + } + } + + return true; +} + +bool DiagnoseBasic::overwriteFiles() const +{ + // QString dirname(qApp->property("dataPath").toString() + "/overwrite"); + QString dirname(m_MOInfo->overwritePath()); + if (m_MOInfo->pluginSetting(name(), "ow_ignore_empty").toBool() || + m_MOInfo->pluginSetting(name(), "ow_ignore_log").toBool()) { + return !checkEmpty(dirname); + } + QDir dir(dirname); + bool checkDirs = m_MOInfo->managedGame()->getModMappings().keys().size() > 1 || + m_MOInfo->managedGame()->getModMappings().keys().first() != ""; + if (checkDirs) { + bool empty = true; + for (auto dir : m_MOInfo->managedGame()->getModMappings().keys()) { + auto mapDir = QDir(dirname).filePath(dir); + if (QDir(mapDir).exists()) { + empty = QDir(mapDir).count() == 2; // account for . and .. + } + if (!empty) + break; + } + return !empty; + } + return dir.count() != 2; // account for . and .. +} + +bool DiagnoseBasic::nitpickInstalled() const +{ + QString path = m_MOInfo->resolvePath("skse/plugins/nitpick.dll"); + + return !path.isEmpty(); +} + +/// unused code to remove duplicates from a vector +template +void makeUnique(std::vector& vector) +{ + std::set done; + + auto read = vector.begin(); + auto write = vector.begin(); + + for (; read != vector.end(); ++read) { + if (done.insert(*read).second) { + *write = *read; + ++write; + } + } + + vector.erase(write, vector.end()); +} + +bool DiagnoseBasic::missingMasters() const +{ + std::set enabledPlugins; + + QStringList esps = m_MOInfo->findFiles("", [](const QString& fileName) -> bool { + return fileName.endsWith(".esp", FileNameComparator::CaseSensitivity) || + fileName.endsWith(".esm", FileNameComparator::CaseSensitivity) || + fileName.endsWith(".esl", FileNameComparator::CaseSensitivity); + }); + // gather enabled masters first + for (const QString& esp : esps) { + QString baseName = QFileInfo(esp).fileName(); + if (m_MOInfo->pluginList()->state(baseName) == IPluginList::STATE_ACTIVE) { + enabledPlugins.insert(baseName.toLower()); + } + } + + m_MissingMasters.clear(); + m_PluginChildren.clear(); + // for each required master in each esp, test if it's in the list of enabled masters. + for (const QString& esp : esps) { + QString baseName = QFileInfo(esp).fileName(); + if (m_MOInfo->pluginList()->state(baseName) == IPluginList::STATE_ACTIVE) { + for (const QString master : m_MOInfo->pluginList()->masters(baseName)) { + if (enabledPlugins.find(master.toLower()) == enabledPlugins.end()) { + m_MissingMasters.insert(master); + m_PluginChildren[master].insert(baseName); + } + } + } + } + return !m_MissingMasters.empty(); +} + +bool DiagnoseBasic::alternateGame() const +{ + QStringList mods = m_MOInfo->modList()->allMods(); + for (QString mod : mods) { + if (m_MOInfo->modList()->state(mod) & MOBase::IModList::STATE_ALTERNATE && + m_MOInfo->modList()->state(mod) & MOBase::IModList::STATE_ACTIVE) + return true; + } + return false; +} + +bool DiagnoseBasic::invalidFontConfig() const +{ + if ((m_MOInfo->managedGame()->gameName() != "Skyrim") && + (m_MOInfo->managedGame()->gameShortName() != "SkyrimSE")) { + // this check is only for skyrim + return false; + } + + // files from skyrim_interface.bsa + static std::vector defaultFonts{"interface\\fonts_console.swf", + "interface\\fonts_en.swf", + "interface\\fonts_cclub.swf"}; + + QString configPath = m_MOInfo->resolvePath("interface/fontconfig.txt"); + if (configPath.isEmpty()) { + return false; + } + QFile config(configPath); + if (!config.open(QIODevice::ReadOnly | QIODevice::Text)) { + qDebug("failed to open %s", qUtf8Printable(configPath)); + return false; + } + + std::regex exp("^fontlib \"([^\"]*)\"$"); + while (!config.atEnd()) { + QByteArray row = config.readLine(); + std::cmatch match; + if (std::regex_search(row.constData(), match, exp)) { + std::string temp = match[1]; + QString path(temp.c_str()); + bool isDefault = false; + for (const QString& def : defaultFonts) { + if (QString::compare(def, path, FileNameComparator::CaseSensitivity) == 0) { + isDefault = true; + break; + } + } + + if (!isDefault && m_MOInfo->resolvePath(path).isEmpty()) { + return true; + } + } + } + return false; +} + +static bool checkFileAttributes(const QString& path) +{ +#ifdef _WIN32 + WCHAR w_path[32767]; + memset(w_path, 0, sizeof(w_path)); + path.toWCharArray(w_path); + + DWORD attrs = GetFileAttributes(w_path); + if (attrs != INVALID_FILE_ATTRIBUTES) { + if (!(attrs & FILE_ATTRIBUTE_ARCHIVE) && !(attrs & FILE_ATTRIBUTE_NORMAL) && + (attrs & ~(FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_ARCHIVE))) { + QString debug; + debug += QString("%1 ").arg(attrs, 8, 16, QLatin1Char('0')); + + // A C D H I O P R S U V X Z + debug += (attrs & FILE_ATTRIBUTE_DIRECTORY) ? "D" : " "; + debug += (attrs & FILE_ATTRIBUTE_ARCHIVE) ? "A" : " "; + debug += (attrs & FILE_ATTRIBUTE_READONLY) ? "R" : " "; + debug += (attrs & FILE_ATTRIBUTE_SYSTEM) ? "S" : " "; + debug += (attrs & FILE_ATTRIBUTE_HIDDEN) ? "H" : " "; + debug += (attrs & FILE_ATTRIBUTE_OFFLINE) ? "O" : " "; + debug += (attrs & FILE_ATTRIBUTE_NOT_CONTENT_INDEXED) ? "I" : " "; + debug += (attrs & FILE_ATTRIBUTE_NO_SCRUB_DATA) ? "X" : " "; + debug += (attrs & FILE_ATTRIBUTE_INTEGRITY_STREAM) ? "V" : " "; + debug += (attrs & FILE_ATTRIBUTE_PINNED) ? "P" : " "; + debug += (attrs & FILE_ATTRIBUTE_UNPINNED) ? "U" : " "; + debug += (attrs & FILE_ATTRIBUTE_COMPRESSED) ? "C" : " "; + debug += (attrs & FILE_ATTRIBUTE_SPARSE_FILE) ? "Z" : " "; + + debug += QString(" %1").arg(path); + + qDebug() << debug; + + return true; + } + } else { + DWORD error = ::GetLastError(); + qWarning(qUtf8Printable(QString("Unable to get file attributes for %1 (error %2)") + .arg(w_path) + .arg(error))); + } + return false; +#else + Q_UNUSED(path); + return false; +#endif +} + +static bool fixFileAttributes(const QString& path) +{ +#ifdef _WIN32 + bool success = true; + + WCHAR w_path[32767]; + memset(w_path, 0, sizeof(w_path)); + path.toWCharArray(w_path); + + DWORD attrs = GetFileAttributes(w_path); + if (attrs != INVALID_FILE_ATTRIBUTES) { + // Clear all the attributes possible, except ARCHIVE, with SetFileAttributes + if (!SetFileAttributes( + w_path, attrs & FILE_ATTRIBUTE_ARCHIVE ? FILE_ATTRIBUTE_ARCHIVE : 0)) { + DWORD error = GetLastError(); + qWarning(qUtf8Printable(QString("Unable to set file attributes for %1 (error %2)") + .arg(path) + .arg(error))); + success = false; + } + + // Compression requires DeviceIoControl + if (attrs & FILE_ATTRIBUTE_COMPRESSED) { + HANDLE hndl = CreateFile(w_path, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + if (hndl != INVALID_HANDLE_VALUE) { + USHORT compressionSetting = COMPRESSION_FORMAT_NONE; + DWORD bytesReturned = 0; + if (!DeviceIoControl(hndl, FSCTL_SET_COMPRESSION, &compressionSetting, + sizeof(compressionSetting), NULL, 0, &bytesReturned, + NULL)) { + DWORD error = GetLastError(); + qWarning(qUtf8Printable( + QString("Unable to disable compression for file %1 (error %2)") + .arg(path) + .arg(error))); + success = false; + } + CloseHandle(hndl); + } else { + DWORD error = GetLastError(); + qWarning(qUtf8Printable( + QString("Unable to open file %1 (error %2)").arg(path).arg(error))); + success = false; + } + } + + // Sparseness requires DeviceIoControl + if (attrs & FILE_ATTRIBUTE_SPARSE_FILE) { + HANDLE hndl = CreateFile(w_path, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + if (hndl != INVALID_HANDLE_VALUE) { + FILE_SET_SPARSE_BUFFER setting = {FALSE}; + DWORD bytesReturned = 0; + if (!DeviceIoControl(hndl, FSCTL_SET_SPARSE, &setting, sizeof(setting), NULL, 0, + &bytesReturned, NULL)) { + DWORD error = GetLastError(); + qWarning(qUtf8Printable( + QString("Unable to disable sparseness for file %1 (error %2)") + .arg(path) + .arg(error))); + success = false; + } + CloseHandle(hndl); + } else { + DWORD error = GetLastError(); + qWarning(qUtf8Printable( + QString("Unable to open file %1 (error %2)").arg(path).arg(error))); + success = false; + } + } + + // As a last ditch effort, set the archive flag + if (!success) { + attrs = GetFileAttributes(w_path); + if (attrs != INVALID_FILE_ATTRIBUTES) { + if (!SetFileAttributes(w_path, attrs | FILE_ATTRIBUTE_ARCHIVE)) { + DWORD error = GetLastError(); + qWarning( + qUtf8Printable(QString("Unable to set file attributes for %1 (error %2)") + .arg(path) + .arg(error))); + } else { + success = true; + } + } else { + DWORD error = GetLastError(); + qWarning( + qUtf8Printable(QString("Unable to get file attributes for %1 (error %2)") + .arg(path) + .arg(error))); + } + } + } else { + DWORD error = GetLastError(); + qWarning(qUtf8Printable(QString("Unable to get file attributes for %1 (error %2)") + .arg(path) + .arg(error))); + success = false; + } + + return success; +#else + Q_UNUSED(path); + return true; +#endif +} + +bool DiagnoseBasic::fileAttributes(const QString& executable) const +{ + if (!m_MOInfo->pluginSetting(name(), "check_fileattributes").toBool()) + return true; + + QStringList filesToFix; + QStringList directoriesToSearch; + + // Search the game directory for problems + directoriesToSearch << m_MOInfo->managedGame()->dataDirectory().absolutePath(); + + // Find the active mods to search them too + for (QString mod : m_MOInfo->modList()->allMods()) { + if (m_MOInfo->modList()->state(mod) & MOBase::IModList::STATE_ACTIVE) { + directoriesToSearch << m_MOInfo->modList()->getMod(mod)->absolutePath(); + } + } + + // Find the subdirectories of all the directories + for (int i = 0; i < directoriesToSearch.length(); i++) { + for (QString dir : + QDir(directoriesToSearch[i]) + .entryList(QDir::Hidden | QDir::AllDirs | QDir::NoDotAndDotDot)) { + directoriesToSearch << QDir(directoriesToSearch[i]).filePath(dir); + } + } + + // Set up a progress bar since this can take a while + QPushButton* progressButton = new QPushButton("Cancel"); + progressButton->setEnabled(false); + + QLabel* progressLabel = new QLabel; + progressLabel->setText(tr("File attribute checker\nSearching for problems...")); + progressLabel->setAlignment(Qt::AlignCenter); + + QProgressDialog dialog; + dialog.setWindowModality(Qt::ApplicationModal); + dialog.setCancelButton(progressButton); + dialog.setLabel(progressLabel); + dialog.setMinimumDuration(1); + dialog.show(); + dialog.setMaximum(directoriesToSearch.length()); + + // Find problems with the directories and files + for (int i = 0; i < directoriesToSearch.length(); i++) { + QString* dirPath = &directoriesToSearch[i]; +#ifdef _WIN32 + QString fixedPath = QString("\\\\?\\%1").arg(QDir::toNativeSeparators(*dirPath)); +#else + QString fixedPath = *dirPath; +#endif + if (checkFileAttributes(fixedPath)) + filesToFix << fixedPath; + + for (QString file : QDir(*dirPath).entryList(QDir::Hidden | QDir::AllEntries | + QDir::NoDotAndDotDot)) { +#ifdef _WIN32 + fixedPath = + QString("\\\\?\\%1").arg(QDir::toNativeSeparators(*dirPath + "\\" + file)); +#else + fixedPath = QDir(*dirPath).filePath(file); +#endif + if (checkFileAttributes(fixedPath)) + filesToFix << fixedPath; + } + dialog.setValue(i); + } + + if (filesToFix.length() == 0) { + return true; + } + + QMessageBox::StandardButton userResponse = + QMessageBox::question(nullptr, tr("Invalid file attributes found"), + tr("One or more of your files has attributes that may " + "prevent the game from reading the files. " + "This can result in missing plugins, missing textures, " + "and other such problems.\n\n" + "Fix the file attributes?"), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (userResponse == QMessageBox::Cancel) { + qDebug() << "User canceled executable launch due to file attributes"; + return false; + } else if (userResponse == QMessageBox::No) { + return true; + } + + // Reset progress bar + progressLabel->setText(tr("File attribute checker\nFixing file attributes...")); + dialog.setValue(0); + dialog.setMaximum(filesToFix.length()); + + // Start iterating through files fixing problems + bool success = true; + for (int i = 0; i < filesToFix.length(); i++) { + if (!fixFileAttributes(filesToFix[i])) + success = false; + dialog.setValue(i); + } + + if (!success) { + if (QMessageBox::question(nullptr, tr("Unable to set file attributes"), + tr("Mod Organizer was unable to fix the file attributes " + "of at least one file.\n\n" + "Continue launching %1?") + .arg(executable), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + return false; + } + } + + return true; +} + +std::vector DiagnoseBasic::activeProblems() const +{ + std::vector result; + + if (m_MOInfo->pluginSetting(name(), "check_errorlog").toBool() && errorReported()) { + result.push_back(PROBLEM_ERRORLOG); + } + if (m_MOInfo->pluginSetting(name(), "check_overwrite").toBool() && overwriteFiles()) { + result.push_back(PROBLEM_OVERWRITE); + } + if (m_MOInfo->pluginSetting(name(), "check_font").toBool() && invalidFontConfig()) { + result.push_back(PROBLEM_INVALIDFONT); + } + if (m_MOInfo->pluginSetting(name(), "check_conflict").toBool() && + nitpickInstalled()) { + result.push_back(PROBLEM_NITPICKINSTALLED); + } + if (m_MOInfo->pluginSetting(name(), "check_missingmasters").toBool() && + missingMasters()) { + result.push_back(PROBLEM_MISSINGMASTERS); + } + if (m_MOInfo->pluginSetting(name(), "check_alternategames").toBool() && + alternateGame()) { + result.push_back(PROBLEM_ALTERNATE); + } + if (QFile::exists(m_MOInfo->profilePath() + "/profile_tweaks.ini")) { + result.push_back(PROBLEM_PROFILETWEAKS); + } + + return result; +} + +QString DiagnoseBasic::shortDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_ERRORLOG: + return tr("There was an error reported recently"); + case PROBLEM_OVERWRITE: + return tr("There are files in your Overwrite mod directory"); + case PROBLEM_INVALIDFONT: + return tr("Your font configuration may be broken"); + case PROBLEM_NITPICKINSTALLED: + return tr("Nitpick installed"); + case PROBLEM_PROFILETWEAKS: + return tr("INI Tweaks overwritten"); + case PROBLEM_MISSINGMASTERS: + return tr("Missing Masters"); + case PROBLEM_ALTERNATE: + return tr("At least one unverified mod is using an alternative game source"); + default: + throw MyException(tr("invalid problem key %1").arg(key)); + } +} + +QString DiagnoseBasic::fullDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_ERRORLOG: + return "" + m_ErrorMessage.replace("\n", "
") + "
"; + case PROBLEM_OVERWRITE: + return tr( + "There are currently files in your Overwrite directory. These files are typically newly " + "created files, usually generated by an external mod tool (i.e. Wrye Bash, " + "xEdit, FNIS, ...). Creation Club ESL files will also end up here when " + "downloaded. Any files in Overwrite " + "will take top priority when loading your mod files and will always overwrite " + "any other mod in your profile.
" + "
" + "It is recommended that you review the files in Overwrite and move any relevant files to a new or existing " + "mod. You can do this by double-clicking the Overwrite mod and dragging files from the Overwrite window to " + "a mod entry in the main mod list. It is also possible to move all current " + "Overwrite files to a new mod by " + "right-clicking on the Overwrite mod.
" + "
" + "Not all files in Overwrite need to " + "be removed, but there are several reasons to do so. Some generated files will " + "be directly related to the active mods in your profile and will be " + "incompatible with different mod setups. Since Overwrite is always active, this could cause conflicts between " + "profiles. Additionally, moving relevant game files into a normal mod will " + "give you greater control over those files. Some files can live safely in " + "Overwrite, such as basic logs and " + "cache files. It is up to you to understand how best to manage these files.
" + "
" + "If you do not wish to see this warning and understand how to handle your " + "Overwrite directory, you can open " + "the Mod Organizer settings and disable this warning under the \"Diagnose " + "Basic\" plugin configuration."); + case PROBLEM_INVALIDFONT: + return tr("Your current configuration seems to reference a font that is not " + "installed. You may see only boxes instead of letters.
" + "The font configuration is in Data\\interface\\fontconfig.txt. Most " + "likely you have a broken installation of a font replacer mod."); + case PROBLEM_NITPICKINSTALLED: + return tr("You have the nitpick skse plugin installed. This plugin is not needed " + "with Mod Organizer because MO already offers the same functionality. " + "Worse: The two solutions may conflict so it's strongly suggested you " + "remove this plugin."); + case PROBLEM_PROFILETWEAKS: { + QString fileContent = readFileText(m_MOInfo->profilePath() + "/profile_tweaks.ini"); + return tr("Settings provided in ini tweaks have been overwritten in-game or in an " + "applications.
" + "These overwrites are stored in a separate file " + "(profile_tweaks.ini within the profile directory)
" + "to keep ini-tweaks in their original state but you should really get " + "rid of this file as there is
" + "no tool support in MO to work on it.
" + "Advice: Copy settings you want to keep to an appropriate ini tweak, " + "then delete profile_tweaks.ini.
" + "Hitting the Fix button will delete that file") + + "
profile_tweaks.ini:
" + fileContent + "
"; + } break; + case PROBLEM_MISSINGMASTERS: { + QString masterInfo; + for (auto master : m_MissingMasters) { + masterInfo += ""; + masterInfo += "" + master + ""; + masterInfo += "" + + SetJoin(m_PluginChildren[master], ", ") + ""; + masterInfo += ""; + } + return tr("The masters for some plugins (esp/esl/esm) are not enabled.
" + "The game will crash unless you install and enable the following " + "plugins: ") + + "
" + "" + + "" + masterInfo + "
" + + tr("Master") + "" + tr("Required By") + + "
"; + } break; + case PROBLEM_ALTERNATE: { + return tr( + "You have at least one active mod installed from an alternative game " + "source.
" + "This means that the mod was downloaded from a game source which does not " + "match
" + "the expected primary game.

" + "Depending on the type of mod, this may require converting various files to " + "run correctly.

" + "Advice: Once you have verified the mod is working correctly, you can use the " + "context menu
" + "and select \"Mark as converted/working\" to remove the flag and warning."); + } break; + default: + throw MyException(tr("invalid problem key %1").arg(key)); + } +} + +bool DiagnoseBasic::hasGuidedFix(unsigned int key) const +{ + return (key == PROBLEM_PROFILETWEAKS); +} + +void DiagnoseBasic::startGuidedFix(unsigned int key) const +{ + switch (key) { + case PROBLEM_PROFILETWEAKS: { + shellDeleteQuiet(m_MOInfo->profilePath() + "/profile_tweaks.ini"); + } break; + default: + throw MyException(tr("invalid problem key %1").arg(key)); + } +} diff --git a/libs/diagnose_basic/src/diagnosebasic.h b/libs/diagnose_basic/src/diagnosebasic.h new file mode 100644 index 0000000..826158a --- /dev/null +++ b/libs/diagnose_basic/src/diagnosebasic.h @@ -0,0 +1,142 @@ +/* +Copyright (C) 2013 Sebastian Herbord. All rights reserved. + +This file is part of basic diagnosis plugin for MO + +This plugin is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This plugin is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this plugin. If not, see . +*/ + +#ifndef DIAGNOSEBASIC_H +#define DIAGNOSEBASIC_H + +#include +#include +#include + +#include +#include +#include +#include + +class DiagnoseBasic : public QObject, + public MOBase::IPlugin, + public MOBase::IPluginDiagnose +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginDiagnose) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.DiagnoseBasic") +#endif + +public: + DiagnoseBasic(); + +public: // IPlugin + virtual bool init(MOBase::IOrganizer* moInfo); + virtual QString name() const; + virtual QString author() const; + virtual QString description() const; + virtual MOBase::VersionInfo version() const; + virtual bool isActive() const; + virtual QList settings() const; + +public: // IPluginDiagnose + virtual std::vector activeProblems() const; + virtual QString shortDescription(unsigned int key) const; + virtual QString fullDescription(unsigned int key) const; + virtual bool hasGuidedFix(unsigned int key) const; + virtual void startGuidedFix(unsigned int key) const; + +private: + bool errorReported() const; + bool overwriteFiles() const; + bool invalidFontConfig() const; + bool nitpickInstalled() const; + bool assetOrder() const; + bool missingMasters() const; + bool alternateGame() const; + bool fileAttributes(const QString& executable) const; + +private: + static const unsigned int PROBLEM_ERRORLOG = 1; + static const unsigned int PROBLEM_OVERWRITE = 2; + static const unsigned int PROBLEM_INVALIDFONT = 3; + static const unsigned int PROBLEM_NITPICKINSTALLED = 4; + static const unsigned int PROBLEM_PROFILETWEAKS = 7; + static const unsigned int PROBLEM_MISSINGMASTERS = 8; + static const unsigned int PROBLEM_ALTERNATE = 9; + + static const unsigned int NUM_CONTEXT_ROWS = 5; + + static const QRegularExpression RE_LOG_FILE; + +private: + struct ListElement + { + QString espName; + QString modName; + int pluginPriority; + int modPriority; + int sortGroup; + bool avoidMove; + QSet relevantScripts; + }; + + struct Move + { + ListElement item; + ListElement reference; + enum EType + { + BEFORE, + AFTER + } type; + Move(const ListElement& initItem, const ListElement& initReference, EType initType) + : item(initItem), reference(initReference), type(initType) + {} + }; + + struct Sorter + { + struct + { + int operator()(const ListElement& lhs, const ListElement& rhs) + { + return lhs.modPriority < rhs.modPriority; + } + } minMod; + + std::vector moves; + + void operator()(std::vector modList); + + private: + void sortGroup(std::vector modList); + }; + + friend bool operator<(const Move& lhs, const Move& rhs); + +private: + void topoSort(std::vector& list) const; + bool checkEmpty(const QString& path) const; + +private: + MOBase::IOrganizer* m_MOInfo; + mutable QString m_ErrorMessage; + mutable QString m_NewestModlistBackup; + mutable std::set m_MissingMasters; + mutable std::map> m_PluginChildren; +}; + +#endif // DIAGNOSEBASIC_H diff --git a/libs/diagnose_basic/vcpkg.json b/libs/diagnose_basic/vcpkg.json new file mode 100644 index 0000000..0740fee --- /dev/null +++ b/libs/diagnose_basic/vcpkg.json @@ -0,0 +1,23 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673", + "packages": ["mo2-*"] + } + ] + } +} diff --git a/libs/esptk/.clang-format b/libs/esptk/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/esptk/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/esptk/.gitattributes b/libs/esptk/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/esptk/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/esptk/.github/workflows/build.yml b/libs/esptk/.github/workflows/build.yml new file mode 100644 index 0000000..a632005 --- /dev/null +++ b/libs/esptk/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: Build ESPTK + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + + - name: Configure ESPTK build + shell: pwsh + run: | + cmake --preset vs2022-windows "-DCMAKE_INSTALL_PREFIX=install" + + - name: Build ESPTK + run: cmake --build vsbuild --config RelWithDebInfo + + - name: Install ESPTK + run: cmake --install vsbuild --config RelWithDebInfo + + - name: Upload ESPTK artifact + uses: actions/upload-artifact@master + with: + name: esptk + path: ./install diff --git a/libs/esptk/.github/workflows/linting.yml b/libs/esptk/.github/workflows/linting.yml new file mode 100644 index 0000000..8bd995c --- /dev/null +++ b/libs/esptk/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint ESPTK + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/esptk/.gitignore b/libs/esptk/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/esptk/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/esptk/.pre-commit-config.yaml b/libs/esptk/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/esptk/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/esptk/CMakeLists.txt b/libs/esptk/CMakeLists.txt new file mode 100644 index 0000000..4242178 --- /dev/null +++ b/libs/esptk/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.16) + +include(CMakePackageConfigHelpers) + +project(esptk) + +add_subdirectory(src) + +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/mo2-esptk-config.cmake" + INSTALL_DESTINATION "lib/cmake/mo2-esptk" + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO +) + +include(${CMAKE_CURRENT_SOURCE_DIR}/Version.cmake) +set(ESPTK_VERSION + ${ESPTK_MAJOR_VERSION}.${ESPTK_MINOR_VERSION}.${ESPTK_PATCH_VERSION}) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/mo2-esptk-config-version.cmake" + VERSION "${ESPTK_VERSION}" + COMPATIBILITY AnyNewerVersion +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/mo2-esptk-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/mo2-esptk-config-version.cmake + DESTINATION lib/cmake/mo2-esptk +) diff --git a/libs/esptk/CMakePresets.json b/libs/esptk/CMakePresets.json new file mode 100644 index 0000000..fb2f5ab --- /dev/null +++ b/libs/esptk/CMakePresets.json @@ -0,0 +1,36 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4" + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev"], + "name": "vs2022-windows", + "toolset": "v143" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/esptk/README.md b/libs/esptk/README.md new file mode 100644 index 0000000..508322f --- /dev/null +++ b/libs/esptk/README.md @@ -0,0 +1,3 @@ +[![Build status](https://ci.appveyor.com/api/projects/status/9521cwqvajwve52x?svg=true)](https://ci.appveyor.com/project/Modorganizer2/modorganizer-esptk) + +# modorganizer-esptk diff --git a/libs/esptk/Version.cmake b/libs/esptk/Version.cmake new file mode 100644 index 0000000..eca00f7 --- /dev/null +++ b/libs/esptk/Version.cmake @@ -0,0 +1,3 @@ +set(ESPTK_MAJOR_VERSION 1) +set(ESPTK_MINOR_VERSION 4) +set(ESPTK_PATCH_VERSION 0) diff --git a/libs/esptk/cmake/config.cmake.in b/libs/esptk/cmake/config.cmake.in new file mode 100644 index 0000000..b04f9d4 --- /dev/null +++ b/libs/esptk/cmake/config.cmake.in @@ -0,0 +1,3 @@ +@PACKAGE_INIT@ + +include ( "${CMAKE_CURRENT_LIST_DIR}/mo2-esptk-targets.cmake" ) diff --git a/libs/esptk/include/esptk/espexceptions.h b/libs/esptk/include/esptk/espexceptions.h new file mode 100644 index 0000000..9830ed6 --- /dev/null +++ b/libs/esptk/include/esptk/espexceptions.h @@ -0,0 +1,23 @@ +#ifndef ESPEXCEPTIONS_H +#define ESPEXCEPTIONS_H + +#include + +namespace ESP +{ + +class InvalidRecordException : public std::runtime_error +{ +public: + InvalidRecordException(const std::string& message) : std::runtime_error(message) {} +}; + +class InvalidFileException : public std::runtime_error +{ +public: + InvalidFileException(const std::string& message) : std::runtime_error(message) {} +}; + +} // namespace ESP + +#endif // ESPEXCEPTIONS_H diff --git a/libs/esptk/include/esptk/espfile.h b/libs/esptk/include/esptk/espfile.h new file mode 100644 index 0000000..04be76e --- /dev/null +++ b/libs/esptk/include/esptk/espfile.h @@ -0,0 +1,72 @@ +#ifndef ESPFILE_H +#define ESPFILE_H + +#include "record.h" +#include "tes3record.h" +#include +#include +#include + +namespace ESP +{ + +class SubRecord; + +class File +{ +public: + File(const std::string& fileName); + File(const std::wstring& fileName); + + Record readRecord(); + + bool isMaster() const; + bool isLight(bool overlaySupport = false) const; + bool isMedium() const; + bool isOverlay() const; + bool isBlueprint() const; + bool isDummy() const; + uint16_t formVersion() const { return m_MainRecord.formVersion(); }; + float headerVersion() const { return m_Header.version; } + std::string author() const { return m_Author; } + std::string description() const { return m_Description; } + std::set masters() const { return m_Masters; } + +private: + void init(); + + void onHEDR(const SubRecord& rec); + void onMAST(const SubRecord& rec); + void onCNAM(const SubRecord& rec); + void onSNAM(const SubRecord& rec); + +private: + std::ifstream m_File; + + struct + { + float version; + int32_t numRecords; + uint32_t nextObjectId; + } m_Header; + + struct + { + float version; + uint32_t unknown; + char author[32]; + char description[256]; + uint32_t numRecords; + } m_TES3Header; + + Record m_MainRecord; + + std::string m_Author; + std::string m_Description; + + std::set m_Masters; +}; + +} // namespace ESP + +#endif // ESPFILE_H diff --git a/libs/esptk/include/esptk/esptypes.h b/libs/esptk/include/esptk/esptypes.h new file mode 100644 index 0000000..135dde1 --- /dev/null +++ b/libs/esptk/include/esptk/esptypes.h @@ -0,0 +1,20 @@ +#ifndef ESPTYPES_H +#define ESPTYPES_H + +#include +#include + +template +static T readType(std::istream& stream) +{ + union + { + char buffer[sizeof(T)]; + T value; + }; + memset(buffer, 0x42, sizeof(T)); + stream.read(buffer, sizeof(T)); + return value; +} + +#endif // ESPTYPES_H diff --git a/libs/esptk/include/esptk/record.h b/libs/esptk/include/esptk/record.h new file mode 100644 index 0000000..3a2c996 --- /dev/null +++ b/libs/esptk/include/esptk/record.h @@ -0,0 +1,60 @@ +#ifndef RECORD_H +#define RECORD_H + +#include +#include +#include + +namespace ESP +{ + +/** + * @brief record storage class without record-specific information + */ +class Record +{ +public: + enum EFlag + { + FLAG_MASTER = 0x00000001, + FLAG_LIGHT_ALTERNATE = 0x00000100, // SF light flag (FE/FF memory space) + FLAG_LIGHT = 0x00000200, // SSE & FO4 light flag (FE/FF memory space) + FLAG_OVERLAY = 0x00000200, // SF overlay flag (does not claim new memory space, + // overrules light flag) + FLAG_MEDIUM = 0x00000400, // SF Creation update, indicates medium plugin type (FD + // memory space) + FLAG_BLUEPRINT = + 0x0000800, // SF blueprint flag (force loads after all other plugins) + FLAG_COMPRESSED = 0x00040000 + }; + +public: + Record(); + + bool readFrom(std::istream& stream); + + bool flagSet(EFlag flag) const; + + uint16_t formVersion() const { return m_FormVersion; } + + const std::vector& data() const { return m_Data; } + +private: + struct Header + { + char type[4]; + uint32_t dataSize; + uint32_t flags; + uint32_t id; + uint32_t revision; + } m_Header; + + uint16_t m_FormVersion; + std::vector m_Data; + + bool m_OblivionStyle; +}; + +} // namespace ESP + +#endif // RECORD_H diff --git a/libs/esptk/include/esptk/subrecord.h b/libs/esptk/include/esptk/subrecord.h new file mode 100644 index 0000000..f9eb36c --- /dev/null +++ b/libs/esptk/include/esptk/subrecord.h @@ -0,0 +1,44 @@ +#ifndef SUBRECORD_H +#define SUBRECORD_H + +#include +#include +#include + +namespace ESP +{ + +/** + * @brief sub-record storage class without record-specific information + */ +class SubRecord +{ +public: + enum EType + { + TYPE_UNKNOWN, + TYPE_HEDR, + TYPE_CNAM, + TYPE_SNAM, + TYPE_MAST, + TYPE_ONAM + }; + + static const int NUM_TYPES = TYPE_ONAM; + +public: + SubRecord(); + + bool readFrom(std::istream& stream, uint32_t sizeOverride = 0UL); + + EType type() const { return m_Type; } + const std::vector& data() const { return m_Data; } + +private: + EType m_Type; + std::vector m_Data; +}; + +} // namespace ESP + +#endif // SUBRECORD_H diff --git a/libs/esptk/include/esptk/tes3record.h b/libs/esptk/include/esptk/tes3record.h new file mode 100644 index 0000000..19fa040 --- /dev/null +++ b/libs/esptk/include/esptk/tes3record.h @@ -0,0 +1,32 @@ +#ifndef TES3_RECORD_H +#define TES3_RECORD_H + +#include +#include +#include + +namespace ESP +{ + +/** + * @brief record storage class without record-specific information + */ +class TES3Record +{ +public: + TES3Record(); + + bool readFrom(std::istream& stream); + +private: + struct Header + { + char type[4]; + float version; + long unknown; + } m_Header; +}; + +} // namespace ESP + +#endif // TES3_RECORD_H diff --git a/libs/esptk/include/esptk/tes3subrecord.h b/libs/esptk/include/esptk/tes3subrecord.h new file mode 100644 index 0000000..966afcb --- /dev/null +++ b/libs/esptk/include/esptk/tes3subrecord.h @@ -0,0 +1,42 @@ +#ifndef TES3_SUBRECORD_H +#define TES3_SUBRECORD_H + +#include +#include +#include + +namespace ESP +{ + +/** + * @brief sub-record storage class without record-specific information + */ +class TES3SubRecord +{ +public: + enum EType + { + TYPE_UNKNOWN, + TYPE_HEDR, + TYPE_MAST, + TYPE_DATA + }; + + static const int NUM_TYPES = TYPE_DATA; + +public: + TES3SubRecord(); + + bool readFrom(std::istream& stream, uint32_t sizeOverride = 0UL); + + EType type() const { return m_Type; } + const std::vector& data() const { return m_Data; } + +private: + EType m_Type; + std::vector m_Data; +}; + +} // namespace ESP + +#endif // TES3_SUBRECORD_H diff --git a/libs/esptk/src/CMakeLists.txt b/libs/esptk/src/CMakeLists.txt new file mode 100644 index 0000000..8f2b2d3 --- /dev/null +++ b/libs/esptk/src/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(esptk STATIC) + +target_sources(esptk + PRIVATE + espfile.cpp + record.cpp + subrecord.cpp + tes3record.cpp + tes3subrecord.cpp + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_LIST_DIR}/../include + FILES + ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/espexceptions.h + ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/espfile.h + ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/esptypes.h + ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/record.h + ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/subrecord.h + ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/tes3record.h + ${CMAKE_CURRENT_LIST_DIR}/../include/esptk/tes3subrecord.h +) + + +set_target_properties(esptk PROPERTIES CXX_STANDARD 20) + +# for building +target_include_directories(esptk PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../include/esptk) + +if (MSVC) + target_compile_options(esptk + PRIVATE + "/MP" + "/Wall" + "/external:anglebrackets" + "/external:W0" + ) + target_link_options(esptk + PRIVATE + $<$:/LTCG /INCREMENTAL:NO /OPT:REF /OPT:ICF> + ) + + set_target_properties(esptk PROPERTIES VS_STARTUP_PROJECT esptk) +endif() + +add_library(mo2::esptk ALIAS esptk) + +# isntall +install(TARGETS esptk EXPORT esptkTargets FILE_SET HEADERS) +install(EXPORT esptkTargets + FILE mo2-esptk-targets.cmake + NAMESPACE mo2:: + DESTINATION lib/cmake/mo2-esptk +) diff --git a/libs/esptk/src/espfile.cpp b/libs/esptk/src/espfile.cpp new file mode 100644 index 0000000..b97d222 --- /dev/null +++ b/libs/esptk/src/espfile.cpp @@ -0,0 +1,188 @@ +#include "espfile.h" +#include "espexceptions.h" +#include "subrecord.h" +#include "tes3subrecord.h" +#include +#include +#include + +ESP::File::File(const std::string& fileName) +{ + m_File.open(fileName, std::fstream::in | std::fstream::binary); + init(); +} + +ESP::File::File(const std::wstring& fileName) +{ +#ifdef _WIN32 + m_File.open(fileName, std::fstream::in | std::fstream::binary); +#else + // Linux: convert wstring to string (UTF-8) + std::string narrowName(fileName.begin(), fileName.end()); + m_File.open(narrowName, std::fstream::in | std::fstream::binary); +#endif + init(); +} + +class membuf : public std::basic_streambuf +{ +public: + membuf(const char* start, size_t size) + { + // baad me! this is intended for an istream only so we're not modifying + char* startMod = const_cast(start); + setg(startMod, startMod, startMod + size); + } +}; + +void ESP::File::init() +{ + if (!m_File.is_open()) { + throw ESP::InvalidFileException("file not found"); + } + m_File.exceptions(std::ios_base::badbit); + + uint8_t type[4]; + if (!m_File.read(reinterpret_cast(type), 4)) { + throw ESP::InvalidFileException("file incomplete"); + } + if (memcmp(type, "TES3", 4) == 0) { + ESP::TES3Record rec; + rec.readFrom(m_File); + + while (!m_File.eof() && !m_File.fail()) { + ESP::TES3SubRecord subRec; + bool success = subRec.readFrom(m_File); + int headerSize = sizeof(m_TES3Header); + if (success) { + if (subRec.type() != TES3SubRecord::TYPE_UNKNOWN) { + switch (subRec.type()) { + case TES3SubRecord::TYPE_HEDR: + if (subRec.data().size() != sizeof(m_TES3Header)) { + printf("invalid header size\n"); + m_Header.version = 0.0f; + m_Header.numRecords = 1; // prevent this esp appear like a dummy + } else { + memcpy(&m_TES3Header, &subRec.data()[0], sizeof(m_TES3Header)); + } + m_Header.version = m_TES3Header.version; + m_Header.numRecords = m_TES3Header.numRecords; + m_Author = reinterpret_cast(m_TES3Header.author); + m_Description = reinterpret_cast(m_TES3Header.description); + break; + case TES3SubRecord::TYPE_MAST: + if (subRec.data().size() > 0) + m_Masters.insert(reinterpret_cast(&subRec.data()[0])); + break; + } + } + } + } + } else if (memcmp(type, "TES4", 4) == 0) { + m_File.seekg(0); + + m_MainRecord = readRecord(); + + const std::vector& data = m_MainRecord.data(); + if (data.empty()) { + throw ESP::InvalidRecordException("record has no data"); + } + membuf buf(reinterpret_cast(data.data()), data.size()); + + std::istream stream(&buf); + while (!stream.eof() && !stream.fail()) { + SubRecord rec; + bool success = rec.readFrom(stream); + if (success) { + if (rec.type() != SubRecord::TYPE_UNKNOWN) { + switch (rec.type()) { + case SubRecord::TYPE_HEDR: + onHEDR(rec); + break; + case SubRecord::TYPE_MAST: + onMAST(rec); + break; + case SubRecord::TYPE_CNAM: + onCNAM(rec); + break; + case SubRecord::TYPE_SNAM: + onSNAM(rec); + break; + } + } + } + } + } else { + throw ESP::InvalidFileException("invalid file type"); + } +} + +void ESP::File::onHEDR(const SubRecord& rec) +{ + if (rec.data().size() != sizeof(m_Header)) { + printf("invalid header size\n"); + m_Header.version = 0.0f; + m_Header.numRecords = 1; // prevent this esp appear like a dummy + } else { + memcpy(&m_Header, &rec.data()[0], sizeof(m_Header)); + } +} + +void ESP::File::onMAST(const SubRecord& rec) +{ + if (rec.data().size() > 0) + m_Masters.insert(reinterpret_cast(&rec.data()[0])); +} + +void ESP::File::onCNAM(const SubRecord& rec) +{ + if (rec.data().size() > 0) + m_Author = reinterpret_cast(&rec.data()[0]); +} + +void ESP::File::onSNAM(const SubRecord& rec) +{ + if (rec.data().size() > 0) + m_Description = reinterpret_cast(&rec.data()[0]); +} + +ESP::Record ESP::File::readRecord() +{ + ESP::Record rec; + rec.readFrom(m_File); + return rec; +} + +bool ESP::File::isMaster() const +{ + return m_MainRecord.flagSet(Record::FLAG_MASTER); +} + +bool ESP::File::isLight(bool overlaySupport) const +{ + if (overlaySupport) { + return m_MainRecord.flagSet(Record::FLAG_LIGHT_ALTERNATE); + } else { + return m_MainRecord.flagSet(Record::FLAG_LIGHT); + } +} + +bool ESP::File::isMedium() const +{ + return m_MainRecord.flagSet(Record::FLAG_MEDIUM); +} + +bool ESP::File::isOverlay() const +{ + return m_MainRecord.flagSet(Record::FLAG_OVERLAY); +} + +bool ESP::File::isBlueprint() const +{ + return m_MainRecord.flagSet(Record::FLAG_BLUEPRINT); +} + +bool ESP::File::isDummy() const +{ + return m_Header.numRecords == 0; +} diff --git a/libs/esptk/src/record.cpp b/libs/esptk/src/record.cpp new file mode 100644 index 0000000..7c66dc4 --- /dev/null +++ b/libs/esptk/src/record.cpp @@ -0,0 +1,40 @@ +#include "record.h" +#include "espexceptions.h" +#include + +ESP::Record::Record() : m_Header(), m_FormVersion(), m_Data(), m_OblivionStyle(false) {} + +bool ESP::Record::flagSet(ESP::Record::EFlag flag) const +{ + return (m_Header.flags & flag) != 0; +} + +bool ESP::Record::readFrom(std::istream& stream) +{ + if (!stream.read(reinterpret_cast(&m_Header), sizeof(Header))) { + if (stream.gcount() == 0) { + return false; + } else { + throw ESP::InvalidRecordException("record incomplete"); + } + } + + char buf[4]; + stream.read(buf, 4); + if (memcmp(buf, "HEDR", 4) == 0) { + m_OblivionStyle = true; + stream.seekg(-4, std::istream::cur); + // Oblivion-style plugins don't have a form version + } else { + memcpy(&m_FormVersion, buf, sizeof(uint16_t)); + } + + m_Data.resize(m_Header.dataSize); + if (m_Header.dataSize > 0) { + stream.read(reinterpret_cast(m_Data.data()), m_Header.dataSize); + } + if (!stream) { + throw ESP::InvalidRecordException("record incomplete"); + } + return true; +} diff --git a/libs/esptk/src/subrecord.cpp b/libs/esptk/src/subrecord.cpp new file mode 100644 index 0000000..cb3890a --- /dev/null +++ b/libs/esptk/src/subrecord.cpp @@ -0,0 +1,59 @@ +#include "subrecord.h" +#include "espexceptions.h" +#include "esptypes.h" +#include +#include +#include +#include + +ESP::SubRecord::SubRecord() : m_Type(TYPE_UNKNOWN), m_Data() {} + +bool ESP::SubRecord::readFrom(std::istream& stream, uint32_t sizeOverride) +{ + static std::unordered_map s_TypeMap{{"HEDR", TYPE_HEDR}, + {"CNAM", TYPE_CNAM}, + {"MAST", TYPE_MAST}, + {"ONAM", TYPE_ONAM}, + {"SNAM", TYPE_SNAM}}; + + char typeString[5]; + if (!stream.read(typeString, 4)) { + if (stream.gcount() == 0) { + return false; + } else { + throw ESP::InvalidRecordException("sub-record incomplete (unknown type)"); + } + } + if (stream.gcount() != 4) { + throw ESP::InvalidRecordException( + std::string("sub-record type incomplete (invalid type ") + typeString + ")"); + } + typeString[4] = '\0'; // not sure if this is required, shouldn't be + auto iter = s_TypeMap.find(std::string(typeString)); + if (iter != s_TypeMap.end()) { + m_Type = iter->second; + } else if (strncmp(typeString, "XXXX", 4) == 0) { + if (readType(stream) != 4) { + throw ESP::InvalidRecordException( + "XXXX record is supposed to be 4 bytes in size"); + } + return readFrom(stream, readType(stream)); + } else { + m_Type = TYPE_UNKNOWN; + } + + uint32_t dataSize = readType(stream); + if (sizeOverride != 0UL) { + dataSize = sizeOverride; + } + m_Data.resize(dataSize); + + if (dataSize > 0) { + stream.read(reinterpret_cast(m_Data.data()), dataSize); + } + if (!stream) { + throw ESP::InvalidRecordException(std::string("sub-record incomplete: ") + + typeString); + } + return true; +} diff --git a/libs/esptk/src/tes3record.cpp b/libs/esptk/src/tes3record.cpp new file mode 100644 index 0000000..6f0fe60 --- /dev/null +++ b/libs/esptk/src/tes3record.cpp @@ -0,0 +1,16 @@ +#include "tes3record.h" +#include "espexceptions.h" + +ESP::TES3Record::TES3Record() : m_Header() {} + +bool ESP::TES3Record::readFrom(std::istream& stream) +{ + if (!stream.read(reinterpret_cast(&m_Header), sizeof(Header))) { + if (stream.gcount() == 0) { + return false; + } else { + throw ESP::InvalidRecordException("record incomplete"); + } + } + return true; +} diff --git a/libs/esptk/src/tes3subrecord.cpp b/libs/esptk/src/tes3subrecord.cpp new file mode 100644 index 0000000..1f6c137 --- /dev/null +++ b/libs/esptk/src/tes3subrecord.cpp @@ -0,0 +1,52 @@ +#include "tes3subrecord.h" +#include "espexceptions.h" +#include "esptypes.h" +#include +#include +#include + +ESP::TES3SubRecord::TES3SubRecord() : m_Type(TYPE_UNKNOWN), m_Data() {} + +bool ESP::TES3SubRecord::readFrom(std::istream& stream, uint32_t sizeOverride) +{ + static std::unordered_map s_TypeMap = { + {"HEDR", TYPE_HEDR}, {"MAST", TYPE_MAST}, {"DATA", TYPE_DATA}}; + + char typeString[5]; + if (!stream.read(typeString, 4)) { + if (stream.gcount() == 0) { + return false; + } else { + throw ESP::InvalidRecordException("sub-record incomplete (unknown type)"); + } + } + if (stream.gcount() != 4) { + throw ESP::InvalidRecordException( + std::string("sub-record type incomplete (invalid type ") + typeString + ")"); + } + typeString[4] = '\0'; // not sure if this is required, shouldn't be + auto iter = s_TypeMap.find(std::string(typeString)); + uint32_t dataSize; + if (iter != s_TypeMap.end()) { + m_Type = iter->second; + dataSize = readType(stream); + } else { + m_Type = TYPE_UNKNOWN; + dataSize = readType(stream); + stream.seekg(8, std::istream::cur); + } + + if (sizeOverride != 0UL) { + dataSize = sizeOverride; + } + m_Data.resize(dataSize); + + if (dataSize > 0) { + stream.read(reinterpret_cast(m_Data.data()), dataSize); + } + if (!stream) { + throw ESP::InvalidRecordException(std::string("sub-record incomplete: ") + + typeString); + } + return true; +} diff --git a/libs/fnistool/.editorconfig b/libs/fnistool/.editorconfig new file mode 100644 index 0000000..f64592a --- /dev/null +++ b/libs/fnistool/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.py] +indent_style = space +indent_size = 4 + +[{CMakeLists.txt, *.cmake}] +indent_style = space +indent_size = 4 diff --git a/libs/fnistool/.gitignore b/libs/fnistool/.gitignore new file mode 100644 index 0000000..5afe969 --- /dev/null +++ b/libs/fnistool/.gitignore @@ -0,0 +1,109 @@ +# Mod Organizer Umbrella stuff +/msbuild.log +/*std*.log +/*build + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/libs/fnistool/.pre-commit-config.yaml b/libs/fnistool/.pre-commit-config.yaml new file mode 100644 index 0000000..4b7d480 --- /dev/null +++ b/libs/fnistool/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/fnistool/CMakeLists.txt b/libs/fnistool/CMakeLists.txt new file mode 100644 index 0000000..01f7443 --- /dev/null +++ b/libs/fnistool/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(FNISTool LANGUAGES NONE) + +add_subdirectory(src) diff --git a/libs/fnistool/CMakePresets.json b/libs/fnistool/CMakePresets.json new file mode 100644 index 0000000..acc6d97 --- /dev/null +++ b/libs/fnistool/CMakePresets.json @@ -0,0 +1,17 @@ +{ + "configurePresets": [ + { + "binaryDir": "${sourceDir}/vsbuild", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "generator": "Visual Studio 17 2022", + "name": "vs2022-windows" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/fnistool/LICENSE b/libs/fnistool/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/libs/fnistool/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/libs/fnistool/src/CMakeLists.txt b/libs/fnistool/src/CMakeLists.txt new file mode 100644 index 0000000..84c2fc2 --- /dev/null +++ b/libs/fnistool/src/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(mo2-cmake CONFIG REQUIRED) + +add_custom_target(FNISTool ALL) +mo2_configure_python(FNISTool SIMPLE) diff --git a/libs/fnistool/src/FNISPatches.py b/libs/fnistool/src/FNISPatches.py new file mode 100644 index 0000000..87fdbf0 --- /dev/null +++ b/libs/fnistool/src/FNISPatches.py @@ -0,0 +1,245 @@ +# This Mod Organizer plugin is released to the pubic under the terms of the GNU GPL version 3, which is accessible from the Free Software Foundation here: https://www.gnu.org/licenses/gpl-3.0-standalone.html + +# To use this plugin, place it in the plugins directory of your Mod Organizer install. You will then find a 'ENTER BUTTON NAME HERE BEFORE RELEASE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' option under the tools menu. + +# Intended behaviour: +# * Adds button to tools menu. +# * If FNIS' location isn't known (or isn't valid, e.g. FNIS isn't actually there) when the button is pressed, nag the user to run the main FNIS Tool at least once, and exit. +# * Reads FNIS' patches file for the current game and the MyPatches.txt file to determine which are active. +# * Displays a popup where the user can enable or disable patches, and saves the results to MyPatches.txt when Save is pressed. + +# Future behaviour: +# * Maybe allow the user to show hidden patches +# * Maybe move the saved list to something stored per-profile + +import os +import pathlib +import sys + +from FNISTool import FNISTool +from PyQt6.QtCore import QCoreApplication, qCritical, QFileInfo, Qt +from PyQt6.QtGui import QIcon, QFileSystemModel +from PyQt6.QtWidgets import QDialogButtonBox, QLabel, QListWidget, QListWidgetItem, QMessageBox, QVBoxLayout, QDialog + +import mobase + +patchListNames = { + "Skyrim": "PatchList.txt", + "Skyrim Special Edition": "PatchListSE.txt", + "Skyrim VR": "PatchListVR.txt" +} + +class Patch: + + def __init__(self, fullString): + fields = fullString.split('#') + # field names taken from FNIS PatchListSE.txt + self.patchid = fields[0] + self.hidden = fields[1] == "1" + self.num_bones = fields[2] + self.required_behaviors_pattern = fields[3] + self.text_for_patch = fields[4] + if len(fields) > 5: + self.optional_file_path_for_mod_install_check = fields[5] + else: + self.optional_file_path_for_mod_install_check = None + + def asQListWidgetItem(self): + listItem = QListWidgetItem(self.text_for_patch, None, 0) + listItem.setFlags(listItem.flags() | Qt.ItemFlag.ItemIsUserCheckable) + # We can't set the hidden status until the item is actually in a list + listItem.setData(Qt.ItemDataRole.UserRole, self.patchid) + + return listItem + +class ExpandingQListWidget(QListWidget): + + def __init__(self, parent=None): + super().__init__(parent) + + def sizeHint(self): + return self.childrenRect().size() + +class FNISPatches(mobase.IPluginTool): + + def __init__(self): + super(FNISPatches, self).__init__() + self.__organizer = None + self.__parentWidget = None + + def init(self, organizer): + self.__organizer = organizer + if sys.version_info < (3, 0): + qCritical(self.tr("FNISPatches plugin requires a Python 3 interpreter, but is running on a Python 2 interpreter.")) + QMessageBox.critical(self.__parentWidget, self.tr("Incompatible Python version."), self.tr("This version of the FNIS Patches plugin requires a Python 3 interpreter, but Mod Organizer has provided a Python 2 interpreter. You should check for an updated version, including in the Mod Organizer 2 Development Discord Server.")) + return False + return True + + def name(self): + return "FNIS Patches Tool" + + def localizedName(self): + return self.tr("FNIS Patches Tool") + + def author(self): + return "AnyOldName3" + + def description(self): + return self.tr("Configures the patches which FNIS applies to the game.") + + def version(self): + return mobase.VersionInfo(1, 0, 1, mobase.ReleaseType.final) + + def master(self): + return "FNIS Integration Tool" + + def settings(self): + return [] + + def displayName(self): + return self.tr("FNIS/Configure FNIS Patches") + + def tooltip(self): + return self.tr("Configures the patches which FNIS applies to the game.") + + def icon(self): + fnisPath = self.__organizer.pluginSetting(self.__mainToolName(), "fnis-path") + if os.path.exists(fnisPath): + # We can't directly grab the icon from an executable, but this seems like the simplest alternative. + fin = QFileInfo(fnisPath) + model = QFileSystemModel() + model.setRootPath(fin.path()) + return model.fileIcon(model.index(fin.filePath())) + else: + # Fall back to where the user might have put an icon manually. + return QIcon("plugins/FNIS.ico") + + def setParentWidget(self, widget): + self.__parentWidget = widget + + def display(self): + fnisPath = self.__getFNISPath() + if fnisPath == None: + return + + enabledPatches = self.__loadEnabledPatches() + + availablePatches = self.__loadAvailablePatches() + + dialog = QDialog(self.__parentWidget) + dialog.setWindowTitle(self.tr("Select Patches")) + + label = QLabel(self.tr("Note: Some patches may be automatically enabled or disabled by Fore's New Idles in Skyrim, so don't be surprised if its list differs from this one.")) + label.setWordWrap(True) + + listWidget = ExpandingQListWidget() + for patch in availablePatches.values(): + listItem = patch.asQListWidgetItem() + if patch.patchid in enabledPatches: + listItem.setCheckState(Qt.CheckState.Checked) + else: + listItem.setCheckState(Qt.CheckState.Unchecked) + listWidget.addItem(listItem) + listItem.setHidden(patch.hidden) + + buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel) + buttonBox.accepted.connect(dialog.accept) + buttonBox.rejected.connect(dialog.reject) + + layout = QVBoxLayout() + layout.addWidget(label) + layout.addWidget(listWidget) + layout.addWidget(buttonBox) + dialog.setLayout(layout) + + result = dialog.exec() + if result == QDialog.DialogCode.Rejected: + # Cancel was pressed + return + + enabledPatches = set() + for i in range(0, listWidget.count()): + listItem = listWidget.item(i) + if listItem.checkState() == Qt.CheckState.Checked: + enabledPatches.add(listItem.data(Qt.ItemDataRole.UserRole)) + self.__saveEnabledPatches(enabledPatches) + + def tr(self, str): + return QCoreApplication.translate("FNISPatches", str) + + @staticmethod + def __mainToolName(): + return FNISTool().name() + + def __getFNISPath(self): + savedPath = self.__organizer.pluginSetting(self.__mainToolName(), "fnis-path") + # FNIS must be installed within the game's data directory, so needs to either be within that or a mod folder + modDirectory = self.__getModDirectory() + gameDataDirectory = pathlib.Path(self.__organizer.managedGame().dataDirectory().absolutePath()) + pathlibPath = pathlib.Path(savedPath) + inGoodLocation = self.__withinDirectory(pathlibPath, modDirectory) + inGoodLocation |= self.__withinDirectory(pathlibPath, gameDataDirectory) + if not pathlibPath.is_file() or not inGoodLocation: + QMessageBox.information(self.__parentWidget, self.tr("Unable to find FNIS"), self.tr("Fore's New Idles in Skyrim can't be found using the location saved in Mod Organizer's settings. Please run the main FNIS integration tool before using this one.")) + return None + return savedPath + + def __getModDirectory(self): + modDirectory = None + modList = self.__organizer.modList().allModsByProfilePriority() + # Get the first managed mod so we can access the mods directory. + for mod in modList: + if (self.__organizer.modList().state(mod) & 0x2) != 0: + modDirectory = pathlib.Path(self.__organizer.modList().getMod(mod).absolutePath()).parent + break + return modDirectory + + @staticmethod + def __withinDirectory(innerPath, outerDir): + for path in innerPath.parents: + if path.samefile(outerDir): + return True + return False + + def __loadEnabledPatches(self, ): + path = pathlib.Path(self.__getFNISPath()).parent + path /= "MyPatches.txt" + if not path.is_file(): + return set() + + enabledPatches = set() + with path.open() as f: + for line in f: + if line != "": + enabledPatches.add(line.strip()) + return enabledPatches + + def __saveEnabledPatches(self, enabledPatches): + path = pathlib.Path(self.__getFNISPath()).parent + path /= "MyPatches.txt" + with path.open('w') as f: + for patchName in enabledPatches: + f.write(patchName) + f.write("\n") + + def __loadAvailablePatches(self): + patchListName = patchListNames[self.__organizer.managedGame().gameName()] + path = pathlib.Path(self.__getFNISPath()).parent + path /= patchListName + availablePatches = {} + # FNIS uses UTF-8 with a BOM, Python assumes everything's using the platform's default encoding + with path.open(encoding="utf-8-sig") as f: + # The first line is a header thing + firstLine = True + for line in f: + if firstLine: + firstLine = False + continue + if line[0] == "'": + continue + patch = Patch(line.strip()) + availablePatches[patch.patchid] = patch + return availablePatches + +def createPlugin(): + return FNISPatches() diff --git a/libs/fnistool/src/FNISTool.py b/libs/fnistool/src/FNISTool.py new file mode 100644 index 0000000..e219bed --- /dev/null +++ b/libs/fnistool/src/FNISTool.py @@ -0,0 +1,342 @@ +# This Mod Organizer plugin is released to the pubic under the terms of the GNU GPL version 3, which is accessible from the Free Software Foundation here: https://www.gnu.org/licenses/gpl-3.0-standalone.html + +# To use this plugin, place it in the plugins directory of your Mod Organizer install. You will then find a 'Run FNIS' option under the tools menu. + +# Intended behaviour: +# * Adds button to tools menu. +# * If FNIS' location isn't known (or isn't valid, e.g. FNIS isn't actually there) when the button is pressed, a file chooser is displayed to find FNIS. +# * `GenerateFNISforUsers.exe RedirectFiles="" InstantExecute=1` is then run within the VFS, with the RedirectFiles option being controlled by other settings (which the user is prompted to fill in if they have not yet been specified). +# * When it exits, if necessary, its return code is used to generate a helpful popup saying whether or not it worked. + +# Future behaviour: +# * As in Vortex's FNIS integration, keeps track of mod files which affect FNIS. +# * If they don't match what was there when the last FNIS output was created, uses IPluginDiagnose interface to display a warning +# * This may already be handled by MO's built-in (but disabled) FNIS checker plugin +import os +import pathlib +import sys + +from PyQt6.QtCore import QCoreApplication, qCritical, QFileInfo +from PyQt6.QtGui import QIcon, QFileSystemModel +from PyQt6.QtWidgets import QFileDialog, QMessageBox + +import mobase + +class FNISMissingException(Exception): + """Thrown if GenerateFNISforUsers.exe path can't be found""" + pass + +class FNISInactiveException(Exception): + """Thrown if GenerateFNISforUsers.exe is installed to an inactive mod""" + pass + +class UnknownOutputPreferenceException(Exception): + """Thrown if the user hasn't specified whether to output to a separate mod""" + pass + +class FNISTool(mobase.IPluginTool): + + def __init__(self): + super(FNISTool, self).__init__() + self.__organizer = None + self.__parentWidget = None + + def init(self, organizer): + self.__organizer = organizer + if sys.version_info < (3, 0): + qCritical(self.tr("FNISTool plugin requires a Python 3 interpreter, but is running on a Python 2 interpreter.")) + QMessageBox.critical(self.__parentWidget, self.tr("Incompatible Python version."), self.tr("This version of the FNIS Integration plugin requires a Python 3 interpreter, but Mod Organizer has provided a Python 2 interpreter. You should check for an updated version, including in the Mod Organizer 2 Development Discord Server.")) + return False + return True + + def name(self): + return "FNIS Integration Tool" + + def localizedName(self): + return self.tr("FNIS Integration Tool") + + def author(self): + return "AnyOldName3" + + def description(self): + return self.tr("Runs GenerateFNISforUsers.exe so the game can load custom animations.") + + def version(self): + return mobase.VersionInfo(1, 2, 0, 0) + + def requirements(self): + return [ + mobase.PluginRequirementFactory.gameDependency({ + "Skyrim", + "Skyrim Special Edition", + "Skyrim VR" + }) + ] + + def settings(self): + return [ + mobase.PluginSetting("fnis-path", self.tr("Path to GenerateFNISforUsers.exe"), ""), + mobase.PluginSetting("output-to-mod", self.tr("Whether or not to direct the FNIS output to a mod folder."), True), + mobase.PluginSetting("output-path", self.tr("When output-to-mod is enabled, the path to the mod to use."), ""), + mobase.PluginSetting("initialised", self.tr("Settings have been initialised. Set to False to reinitialise them."), False), + mobase.PluginSetting("output-logs-to-mod", self.tr("Whether or not to direct any new FNIS logs to a mod folder."), True), + mobase.PluginSetting("output-logs-path", self.tr("When output-logs-to-mod is enabled, the path to the mod to use."), ""), + ] + + def displayName(self): + return self.tr("FNIS/Run FNIS") + + def tooltip(self): + return self.tr("Runs GenerateFNISforUsers.exe so the game can load custom animations.") + + def icon(self): + fnisPath = self.__organizer.pluginSetting(self.name(), "fnis-path") + if os.path.exists(fnisPath): + # We can't directly grab the icon from an executable, but this seems like the simplest alternative. + fin = QFileInfo(fnisPath) + model = QFileSystemModel() + model.setRootPath(fin.path()) + return model.fileIcon(model.index(fin.filePath())) + else: + # Fall back to where the user might have put an icon manually. + return QIcon("plugins/FNIS.ico") + + def setParentWidget(self, widget): + self.__parentWidget = widget + + def display(self): + args = [] + redirectOutput = True + outputModName = None + logOutputModName = "" + + if not bool(self.__organizer.pluginSetting(self.name(), "initialised")): + self.__organizer.setPluginSetting(self.name(), "fnis-path", "") + self.__organizer.setPluginSetting(self.name(), "output-path", "") + self.__organizer.setPluginSetting(self.name(), "output-to-mod", True) + self.__organizer.setPluginSetting(self.name(), "output-logs-path", "") + self.__organizer.setPluginSetting(self.name(), "output-logs-to-mod", True) + + try: + redirectOutput = self.__getRedirectOutput() + except UnknownOutputPreferenceException: + QMessageBox.critical(self.__parentWidget, self.tr("Output preference not set"), self.tr("Whether or not to output to a mod was not specified. The tool will now exit.")) + return + if redirectOutput: + try: + outputPath = self.__getOutputPath() + args.append('RedirectFiles="' + outputPath + '"') + outputModName = pathlib.Path(outputPath).name + except UnknownOutputPreferenceException: + QMessageBox.critical(self.__parentWidget, self.tr("Output mod not set"), self.tr("The mod to output to was not specified. The tool will now exit.")) + return + args.append('InstantExecute=1') + + if redirectOutput: + try: + redirectLogs = self.__getRedirectLogs() + except UnknownOutputPreferenceException: + QMessageBox.critical(self.__parentWidget, self.tr("Output preference not set"), self.tr("Whether or not to output to a mod was not specified. The tool will now exit.")) + return + if redirectLogs: + try: + outputPath = self.__getLogOutputPath() + logOutputModName = pathlib.Path(outputPath).name + except UnknownOutputPreferenceException: + QMessageBox.critical(self.__parentWidget, self.tr("Output mod not set"), self.tr("The mod to output to was not specified. The tool will now exit.")) + return + + try: + executable = self.__getFNISPath() + except FNISMissingException: + QMessageBox.critical(self.__parentWidget, self.tr("FNIS path not specified"), self.tr("The path to GenerateFNISforUsers.exe wasn't specified. The tool will now exit.")) + return + except FNISInactiveException: + # Error has already been displayed, just quit + return + + self.__organizer.setPluginSetting(self.name(), "initialised", True) + + if redirectOutput: + # Disable the output mod as USVFS isn't designed to cope with its input directories being modified + self.__organizer.modList().setActive(outputModName, False) + + if redirectLogs: + # Enable the log output mod + self.__organizer.modList().setActive(logOutputModName, True) + + + handle = self.__organizer.startApplication(executable, args, forcedCustomOverwrite=logOutputModName, ignoreCustomOverwrite=not bool(logOutputModName)) + result, exitCode = self.__organizer.waitForApplication(handle) + + if redirectOutput: + # Enable the output mod + self.__organizer.modList().setActive(outputModName, True) + # Ensure the 'No valid game data' message goes away + self.__organizer.modDataChanged(self.__organizer.getMod(outputModName)) + + if redirectLogs: + self.__organizer.modDataChanged(self.__organizer.getMod(logOutputModName)) + + def tr(self, str): + return QCoreApplication.translate("FNISTool", str) + + def __getRedirectOutput(self): + redirectOutput = bool(self.__organizer.pluginSetting(self.name(), "output-to-mod")) + initialised = bool(self.__organizer.pluginSetting(self.name(), "initialised")) + if not initialised: + result = QMessageBox.question(self.__parentWidget, self.tr("Output to a mod?"), self.tr("Fore's New Idles in Skyrim can output either to Mod Organizer's VFS (potentially overwriting files from multiple mods) or to a separate mod. Would you like FNIS to output to a separate mod? This setting can be updated in the Plugins tab of the Mod Organizer Settings menu."), QMessageBox.StandardButton(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel)) + if result == QMessageBox.StandardButton.Yes: + redirectOutput = True + elif result == QMessageBox.StandardButton.No: + redirectOutput = False + else: + # the user pressed cancel + raise UnknownOutputPreferenceException + + self.__organizer.setPluginSetting(self.name(), "output-to-mod", redirectOutput) + return redirectOutput + + def __getRedirectLogs(self): + redirectLogs = bool(self.__organizer.pluginSetting(self.name(), "output-logs-to-mod")) + initialised = bool(self.__organizer.pluginSetting(self.name(), "initialised")) + if not initialised: + result = QMessageBox.question(self.__parentWidget, self.tr("Output logs to a mod?"), self.tr("Any new logs generated when running FNIS will end up in Mod Organizer's overwrite folder. Would you like these logs to be output to a separate mod? This setting can be updated in the Plugins tab of the Mod Organizer Settings menu."), QMessageBox.StandardButton(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel)) + if result == QMessageBox.StandardButton.Yes: + redirectLogs = True + elif result == QMessageBox.StandardButton.No: + redirectLogs = False + else: + # the user pressed cancel + raise UnknownOutputPreferenceException + + self.__organizer.setPluginSetting(self.name(), "output-logs-to-mod", redirectLogs) + return redirectLogs + + def __getOutputPath(self): + path = self.__organizer.pluginSetting(self.name(), "output-path") + pathlibPath = pathlib.Path(path) + modDirectory = self.__getModDirectory() + isAMod = pathlibPath.parent.samefile(modDirectory) + if not pathlibPath.is_dir() or not isAMod: + QMessageBox.information(self.__parentWidget, self.tr("Choose an output mod"), self.tr("Please choose an output mod for Fore's New Idles in Skyrim. This must be a directory in Mod Organizer's mods directory, and you can create one if you do not have one already. This mod will not be available to the VFS when FNIS is run, so do not choose a mod you use for anything else. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu.")) + while not pathlibPath.is_dir() or not isAMod: + path = QFileDialog.getExistingDirectory(self.__parentWidget, self.tr("Choose an output mod"), str(modDirectory), QFileDialog.Option.ShowDirsOnly) + if not path: + # cancel was pressed + raise UnknownOutputPreferenceException + pathlibPath = pathlib.Path(path) + isAMod = pathlibPath.parent.samefile(modDirectory) + if not isAMod: + QMessageBox.information(self.__parentWidget, self.tr("Not a mod..."), self.tr("The selected directory is not a Mod Organizer managed mod. Please choose a directory within the mods directory.")) + continue + empty = True + for item in pathlibPath.iterdir(): + if item.name != "meta.ini": + empty = False + break + if not empty: + if QMessageBox.question(self.__parentWidget, self.tr("Mod not empty"), self.tr("The selected mod already contains files. Are you sure want to use it as the output mod?"), QMessageBox.StandardButton(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)) == QMessageBox.StandardButton.Yes: + # Proceed normally - the user is happy + pass + else: + # Restart outer loop - the user wants to pick again + isAMod = False + # The user may have created a new mod in the MO mods directory, so we must trigger a refresh + self.__organizer.refreshModList() + self.__organizer.setPluginSetting(self.name(), "output-path", path) + return path + + def __getLogOutputPath(self): + path = self.__organizer.pluginSetting(self.name(), "output-logs-path") + pathlibPath = pathlib.Path(path) + modDirectory = self.__getModDirectory() + fnisOutputPath = pathlib.Path(self.__getOutputPath()) + isAMod = pathlibPath.parent.samefile(modDirectory) + isSameAsFnisOutput = pathlibPath.samefile(fnisOutputPath) + if not pathlibPath.is_dir() or not isAMod or isSameAsFnisOutput: + QMessageBox.information(self.__parentWidget, self.tr("Choose an output mod"), self.tr("Please choose an output mod for logs for Fore's New Idles in Skyrim. This must be a directory in Mod Organizer's mods directory, must not be the same as the FNIS output mod, and you can create one if you do not have one already. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu.")) + while not pathlibPath.is_dir() or not isAMod or isSameAsFnisOutput: + path = QFileDialog.getExistingDirectory(self.__parentWidget, self.tr("Choose a log output mod"), str(modDirectory), QFileDialog.Option.ShowDirsOnly) + if not path: + # cancel was pressed + raise UnknownOutputPreferenceException + pathlibPath = pathlib.Path(path) + isAMod = pathlibPath.parent.samefile(modDirectory) + if not isAMod: + QMessageBox.information(self.__parentWidget, self.tr("Not a mod..."), self.tr("The selected directory is not a Mod Organizer managed mod. Please choose a directory within the mods directory.")) + continue + isSameAsFnisOutput = pathlibPath.samefile(fnisOutputPath) + if isSameAsFnisOutput: + QMessageBox.information(self.__parentWidget, self.tr("Same as FNIS output"), self.tr("The selected mod is the same as the FNIS output mod. Please choose a different mod.")) + continue + empty = True + for item in pathlibPath.iterdir(): + if item.name != "meta.ini": + empty = False + break + if not empty: + if QMessageBox.question(self.__parentWidget, self.tr("Mod not empty"), self.tr("The selected mod already contains files. Are you sure want to use it as the output mod?"), QMessageBox.StandardButton(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)) == QMessageBox.StandardButton.Yes: + # Proceed normally - the user is happy + pass + else: + # Restart outer loop - the user wants to pick again + isAMod = False + + # The user may have created a new mod in the MO mods directory, so we must trigger a refresh + self.__organizer.refreshModList() + self.__organizer.setPluginSetting(self.name(), "output-logs-path", path) + return path + + def __getFNISPath(self): + savedPath = self.__organizer.pluginSetting(self.name(), "fnis-path") + # FNIS must be installed within the game's data directory, so needs to either be within that or a mod folder + modDirectory = self.__getModDirectory() + gameDataDirectory = pathlib.Path(self.__organizer.managedGame().dataDirectory().absolutePath()) + pathlibPath = pathlib.Path(savedPath) + inGoodLocation = self.__withinDirectory(pathlibPath, modDirectory) + inGoodLocation |= self.__withinDirectory(pathlibPath, gameDataDirectory) + if not pathlibPath.is_file() or not inGoodLocation: + QMessageBox.information(self.__parentWidget, self.tr("Find FNIS"), self.tr("Fore's New Idles in Skyrim can't be found using the location saved in Mod Organizer's settings. Please find GenerateFNISforUsers.exe in the file picker. FNIS must be visible within the VFS, so choose an installation either within the game's data directory or within a mod folder. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu.")) + while True: + path = QFileDialog.getOpenFileName(self.__parentWidget, self.tr("Locate GenerateFNISforUsers.exe"), str(modDirectory), "FNIS (GenerateFNISforUsers.exe)")[0] + if path == "": + # Cancel was pressed + raise FNISMissingException + pathlibPath = pathlib.Path(path) + inGoodLocation = self.__withinDirectory(pathlibPath, modDirectory) + inGoodLocation |= self.__withinDirectory(pathlibPath, gameDataDirectory) + if pathlibPath.is_file() and inGoodLocation: + self.__organizer.setPluginSetting(self.name(), "fnis-path", path) + savedPath = path + break + else: + QMessageBox.information(self.__parentWidget, self.tr("Not a compatible location..."), self.tr("Fore's New Idles in Skyrim only works when within the VFS, so must be installed to the game's data directory or within a mod folder. Please select a different FNIS installation.")) + # Check the mod is actually enabled + if self.__withinDirectory(pathlibPath, modDirectory): + fnisModName = None + for path in pathlibPath.parents: + if path.parent.samefile(modDirectory): + fnisModName = path.name + break + if (self.__organizer.modList().state(fnisModName) & mobase.ModState.active) == 0: + # FNIS is installed to an inactive mod + result = QMessageBox.question(self.__parentWidget, self.tr("FNIS mod deactivated"), self.tr("Fore's New Idles in Skyrim is installed to an inactive mod. Press OK to activate it or Cancel to quit the tool"), QMessageBox.StandardButton(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel)) + if result == QMessageBox.StandardButton.Ok: + self.__organizer.modList().setActive(fnisModName, True) + else: + raise FNISInactiveException + return savedPath + + def __getModDirectory(self): + return self.__organizer.modsPath() + + @staticmethod + def __withinDirectory(innerPath, outerDir): + for path in innerPath.parents: + if path.samefile(outerDir): + return True + return False + +def createPlugin(): + return FNISTool() diff --git a/libs/fnistool/src/FNISToolReset.py b/libs/fnistool/src/FNISToolReset.py new file mode 100644 index 0000000..d1f4bc9 --- /dev/null +++ b/libs/fnistool/src/FNISToolReset.py @@ -0,0 +1,76 @@ +import os +import sys + +from FNISTool import FNISTool +from PyQt6.QtCore import QFileInfo, QCoreApplication +from PyQt6.QtGui import QFileSystemModel, QIcon +from PyQt6.QtWidgets import QMessageBox + +import mobase + +class FNISToolReset(mobase.IPluginTool): + def __init__(self): + super(FNISToolReset, self).__init__() + self.__organizer = None + self.__parentWidget = None + + def init(self, organizer): + self.__organizer = organizer + return True + + def name(self): + return "FNIS Integration Tool Reset" + + def localizedName(self): + return self.tr("FNIS Integration Tool Reset") + + def author(self): + return "LostDragonist" + + def description(self): + return self.tr("Provides an easier way to reset the FNIS integration tool settings when needed.") + + def version(self): + return mobase.VersionInfo(1, 0, 0, 0) + + def master(self): + return "FNIS Integration Tool" + + def settings(self): + return [] + + def displayName(self): + return self.tr("FNIS/Reset FNIS Settings") + + def tooltip(self): + return self.description() + + def icon(self): + fnisPath = self.__organizer.pluginSetting(self.__mainToolName(), "fnis-path") + if os.path.exists(fnisPath): + # We can't directly grab the icon from an executable, but this seems like the simplest alternative. + fin = QFileInfo(fnisPath) + model = QFileSystemModel() + model.setRootPath(fin.path()) + return model.fileIcon(model.index(fin.filePath())) + else: + # Fall back to where the user might have put an icon manually. + return QIcon("plugins/FNIS.ico") + + def setParentWidget(self, widget): + self.__parentWidget = widget + + def display(self): + result = QMessageBox.question(self.__parentWidget, self.tr("Reset settings?"), self.tr("Would you like to reset the options that pop up when you first ran \"{}\"?").format(self.__mainToolName())) + if result == QMessageBox.StandardButton.Yes: + self.__organizer.setPluginSetting(self.__mainToolName(), "initialised", False) + + def tr(self, str): + return QCoreApplication.translate("FNISToolReset", str) + + @staticmethod + def __mainToolName(): + return FNISTool().name() + +def createPlugin(): + return FNISToolReset() diff --git a/libs/fnistool/src/FNISTool_en.ts b/libs/fnistool/src/FNISTool_en.ts new file mode 100644 index 0000000..9ccdbee --- /dev/null +++ b/libs/fnistool/src/FNISTool_en.ts @@ -0,0 +1,295 @@ + + + + + FNISPatches + + + FNISPatches plugin requires a Python 3 interpreter, but is running on a Python 2 interpreter. + + + + + Incompatible Python version. + + + + + This version of the FNIS Patches plugin requires a Python 3 interpreter, but Mod Organizer has provided a Python 2 interpreter. You should check for an updated version, including in the Mod Organizer 2 Development Discord Server. + + + + + FNIS Patches Tool + + + + + + Configures the patches which FNIS applies to the game. + + + + + FNIS/Configure FNIS Patches + + + + + Select Patches + + + + + Note: Some patches may be automatically enabled or disabled by Fore's New Idles in Skyrim, so don't be surprised if its list differs from this one. + + + + + Unable to find FNIS + + + + + Fore's New Idles in Skyrim can't be found using the location saved in Mod Organizer's settings. Please run the main FNIS integration tool before using this one. + + + + + FNISTool + + + FNISTool plugin requires a Python 3 interpreter, but is running on a Python 2 interpreter. + + + + + Incompatible Python version. + + + + + This version of the FNIS Integration plugin requires a Python 3 interpreter, but Mod Organizer has provided a Python 2 interpreter. You should check for an updated version, including in the Mod Organizer 2 Development Discord Server. + + + + + FNIS Integration Tool + + + + + + Runs GenerateFNISforUsers.exe so the game can load custom animations. + + + + + Path to GenerateFNISforUsers.exe + + + + + Whether or not to direct the FNIS output to a mod folder. + + + + + When output-to-mod is enabled, the path to the mod to use. + + + + + Settings have been initialised. Set to False to reinitialise them. + + + + + Whether or not to direct any new FNIS logs to a mod folder. + + + + + When output-logs-to-mod is enabled, the path to the mod to use. + + + + + FNIS/Run FNIS + + + + + + Output preference not set + + + + + + Whether or not to output to a mod was not specified. The tool will now exit. + + + + + + Output mod not set + + + + + + The mod to output to was not specified. The tool will now exit. + + + + + FNIS path not specified + + + + + The path to GenerateFNISforUsers.exe wasn't specified. The tool will now exit. + + + + + Output to a mod? + + + + + Fore's New Idles in Skyrim can output either to Mod Organizer's VFS (potentially overwriting files from multiple mods) or to a separate mod. Would you like FNIS to output to a separate mod? This setting can be updated in the Plugins tab of the Mod Organizer Settings menu. + + + + + Output logs to a mod? + + + + + Any new logs generated when running FNIS will end up in Mod Organizer's overwrite folder. Would you like these logs to be output to a separate mod? This setting can be updated in the Plugins tab of the Mod Organizer Settings menu. + + + + + + + Choose an output mod + + + + + Please choose an output mod for Fore's New Idles in Skyrim. This must be a directory in Mod Organizer's mods directory, and you can create one if you do not have one already. This mod will not be available to the VFS when FNIS is run, so do not choose a mod you use for anything else. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu. + + + + + + Not a mod... + + + + + + The selected directory is not a Mod Organizer managed mod. Please choose a directory within the mods directory. + + + + + + Mod not empty + + + + + + The selected mod already contains files. Are you sure want to use it as the output mod? + + + + + Please choose an output mod for logs for Fore's New Idles in Skyrim. This must be a directory in Mod Organizer's mods directory, must not be the same as the FNIS output mod, and you can create one if you do not have one already. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu. + + + + + Choose a log output mod + + + + + Same as FNIS output + + + + + The selected mod is the same as the FNIS output mod. Please choose a different mod. + + + + + Find FNIS + + + + + Fore's New Idles in Skyrim can't be found using the location saved in Mod Organizer's settings. Please find GenerateFNISforUsers.exe in the file picker. FNIS must be visible within the VFS, so choose an installation either within the game's data directory or within a mod folder. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu. + + + + + Locate GenerateFNISforUsers.exe + + + + + Not a compatible location... + + + + + Fore's New Idles in Skyrim only works when within the VFS, so must be installed to the game's data directory or within a mod folder. Please select a different FNIS installation. + + + + + FNIS mod deactivated + + + + + Fore's New Idles in Skyrim is installed to an inactive mod. Press OK to activate it or Cancel to quit the tool + + + + + FNISToolReset + + + FNIS Integration Tool Reset + + + + + Provides an easier way to reset the FNIS integration tool settings when needed. + + + + + FNIS/Reset FNIS Settings + + + + + Reset settings? + + + + + Would you like to reset the options that pop up when you first ran "{}"? + + + + diff --git a/libs/fnistool/vcpkg.json b/libs/fnistool/vcpkg.json new file mode 100644 index 0000000..7627c1f --- /dev/null +++ b/libs/fnistool/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "c55e3ab33eb170aefb1904ded8809cb88df6bb48" + } + } +} diff --git a/libs/form43_checker/.editorconfig b/libs/form43_checker/.editorconfig new file mode 100644 index 0000000..97e2498 --- /dev/null +++ b/libs/form43_checker/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.py] +indent_style = space +indent_size = 4 diff --git a/libs/form43_checker/.gitignore b/libs/form43_checker/.gitignore new file mode 100644 index 0000000..5afe969 --- /dev/null +++ b/libs/form43_checker/.gitignore @@ -0,0 +1,109 @@ +# Mod Organizer Umbrella stuff +/msbuild.log +/*std*.log +/*build + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/libs/form43_checker/.pre-commit-config.yaml b/libs/form43_checker/.pre-commit-config.yaml new file mode 100644 index 0000000..e00bc0d --- /dev/null +++ b/libs/form43_checker/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/form43_checker/CMakeLists.txt b/libs/form43_checker/CMakeLists.txt new file mode 100644 index 0000000..794cdc5 --- /dev/null +++ b/libs/form43_checker/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.16) + +project(Form43Checker LANGUAGES NONE) +add_subdirectory(src) diff --git a/libs/form43_checker/CMakePresets.json b/libs/form43_checker/CMakePresets.json new file mode 100644 index 0000000..acc6d97 --- /dev/null +++ b/libs/form43_checker/CMakePresets.json @@ -0,0 +1,17 @@ +{ + "configurePresets": [ + { + "binaryDir": "${sourceDir}/vsbuild", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "generator": "Visual Studio 17 2022", + "name": "vs2022-windows" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/form43_checker/LICENSE b/libs/form43_checker/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/libs/form43_checker/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/libs/form43_checker/src/CMakeLists.txt b/libs/form43_checker/src/CMakeLists.txt new file mode 100644 index 0000000..7237e8a --- /dev/null +++ b/libs/form43_checker/src/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(mo2-cmake CONFIG REQUIRED) + +add_custom_target(Form43Checker ALL) +mo2_configure_python(Form43Checker SIMPLE) diff --git a/libs/form43_checker/src/Form43Checker.py b/libs/form43_checker/src/Form43Checker.py new file mode 100644 index 0000000..339f6c9 --- /dev/null +++ b/libs/form43_checker/src/Form43Checker.py @@ -0,0 +1,105 @@ +from pathlib import Path + +import mobase +from PyQt6.QtCore import QCoreApplication + + +class Form43Checker(mobase.IPluginDiagnose): + __organizer: mobase.IOrganizer + __invalidPlugins: list[str] = [] + + def __init__(self): + super().__init__() + + def init(self, organizer: mobase.IOrganizer): + self.__organizer = organizer + return True + + def name(self): + return "Form 43 Plugin Checker" + + def localizedName(self): + return "Form 43 Plugin Checker" + + def author(self): + return "AnyOldName3" + + def description(self): + return self.tr( + "Checks plugins (.ESM/.ESP files) to see if any are lower than Form 44 (Skyrim SE)." + ) + + def version(self): + return mobase.VersionInfo(1, 2, 0, 0, mobase.ReleaseType.FINAL) + + def requirements(self): + return [ + mobase.PluginRequirementFactory.gameDependency("Skyrim Special Edition") + ] + + def settings(self) -> list[mobase.PluginSetting]: + return [] + + def activeProblems(self) -> list[int]: + self.__updateInvalidPlugins() + if self.__invalidPlugins: + return [0] + else: + return [] + + def shortDescription(self, key: int) -> str: + return self.tr("Form 43 (or lower) plugin detected") + + def fullDescription(self, key: int) -> str: + pluginList = self.__listPlugins() + pluginListString = "

• " + ("
• ".join(pluginList)) + outputString = self.tr( + "You have one or more plugins that are not form 44. They are:{0}" + ).format(pluginListString) + outputString += "

" + outputString += self.tr( + "Form 43 (or lower) plugins are modules that were made for Skyrim LE (Oldrim) and have not been " + "properly ported to Skyrim Special Edition, which uses form 44 plugins. This usually results in " + "parts of the mod not working correctly." + "

" + "To be converted, these plugins simply need to be opened and saved with the SSE Creation Kit " + "but their presence can be an indication that a mod was not properly ported to SSE and so " + "can potentially have additional issues." + "

" + "Online guides can have more information on how to correctly convert mods for Skyrim SE.
" + ) + return outputString + + def hasGuidedFix(self, key: int) -> bool: + return False + + def startGuidedFix(self, key: int) -> None: + # Maybe we could use xEdit or something to resave the file? + pass + + def tr(self, value: str): + return QCoreApplication.translate("Form43Checker", value) + + def __testFile(self, path: str) -> bool: + version = self.__getForm(path) + return version != -1 and version < 44 + + def __getForm(self, file: str) -> int: + pluginName = Path(file).name + return self.__organizer.pluginList().formVersion(pluginName) + + def __updateInvalidPlugins(self) -> None: + self.__invalidPlugins.clear() + for file in self.__organizer.findFiles("", "*.es[pm]"): + if self.__testFile(file): + self.__invalidPlugins.append(file) + + def __listPlugins(self) -> list[str]: + return [ + f"{Path(file).name} (form {self.__getForm(file)})" + for file in self.__invalidPlugins + ] + + +def createPlugin(): + return Form43Checker() diff --git a/libs/form43_checker/src/Form43Checker_en.ts b/libs/form43_checker/src/Form43Checker_en.ts new file mode 100644 index 0000000..b758636 --- /dev/null +++ b/libs/form43_checker/src/Form43Checker_en.ts @@ -0,0 +1,27 @@ + + + + + Form43Checker + + + Checks plugins (.ESM/.ESP files) to see if any are lower than Form 44 (Skyrim SE). + + + + + Form 43 (or lower) plugin detected + + + + + You have one or more plugins that are not form 44. They are:{0} + + + + + Form 43 (or lower) plugins are modules that were made for Skyrim LE (Oldrim) and have not been properly ported to Skyrim Special Edition, which uses form 44 plugins. This usually results in parts of the mod not working correctly.<br><br>To be converted, these plugins simply need to be opened and saved with the SSE Creation Kit but their presence can be an indication that a mod was not properly ported to SSE and so can potentially have additional issues.<br><br>Online guides can have more information on how to correctly convert mods for Skyrim SE.<br> + + + + diff --git a/libs/form43_checker/vcpkg.json b/libs/form43_checker/vcpkg.json new file mode 100644 index 0000000..7627c1f --- /dev/null +++ b/libs/form43_checker/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "c55e3ab33eb170aefb1904ded8809cb88df6bb48" + } + } +} diff --git a/libs/game_bethesda/.clang-format b/libs/game_bethesda/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/game_bethesda/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/game_bethesda/.git-blame-ignore-revs b/libs/game_bethesda/.git-blame-ignore-revs new file mode 100644 index 0000000..6b3e971 --- /dev/null +++ b/libs/game_bethesda/.git-blame-ignore-revs @@ -0,0 +1,15 @@ +484afb68695eadfa57f1cf9ffc9b6e166ee20174 +59cc8639ca60de146e19de64f9dbf4380f52bcf3 +dba50ca6cecfd73fc917bf19325947fdc06ab2c3 +a070d542b2185c9568e309e540b8e61178b88ad4 +99cc4566477a37dc07eeb6282ee5e4307a6d1914 +2414eb383ad508992ab17d958f3959dc1a566748 +7cce6c37f23c886f388e98828d496edd6c5d72f6 +d8d13d2f4b54eaa4409ae97c168e0e4600c743c6 +8cd4ed42e757921dcf18e6bf0a9d82fbc586a95a +2055815ed96457fc559b3ab2323403f3b20cbe00 +168c64df2fb816d78e6cda9cec12ac4749ab4497 +64d324bb78c54ed9d6cecb8e21aceb626ee8f36a +c4d7eaab7f15f8b1954cfeb56c3314f9b0e9c7ec +632ce9843dc9c263203ce4f376ebcfb1672cf1c3 +891a7103e31bf928441ef8ae5a45cd1b21f1abb0 diff --git a/libs/game_bethesda/.gitattributes b/libs/game_bethesda/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/game_bethesda/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/game_bethesda/.github/workflows/build.yml b/libs/game_bethesda/.github/workflows/build.yml new file mode 100644 index 0000000..a9c5b0e --- /dev/null +++ b/libs/game_bethesda/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build Game Bethesda Library + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }} + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Game Bethesda + id: build-game-bethesda + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase diff --git a/libs/game_bethesda/.github/workflows/linting.yml b/libs/game_bethesda/.github/workflows/linting.yml new file mode 100644 index 0000000..5a2ca91 --- /dev/null +++ b/libs/game_bethesda/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Game Bethesda + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/game_bethesda/.gitignore b/libs/game_bethesda/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/game_bethesda/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/game_bethesda/.pre-commit-config.yaml b/libs/game_bethesda/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/game_bethesda/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/game_bethesda/CMakeLists.txt b/libs/game_bethesda/CMakeLists.txt new file mode 100644 index 0000000..40736b5 --- /dev/null +++ b/libs/game_bethesda/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 3.16) +project(game_bethesda) + +find_package(Qt6 COMPONENTS Widgets REQUIRED) + +# Compatibility helpers for upstream plugin CMake files that still call mo2-cmake +# functions. We implement only what this Linux port needs. +function(mo2_configure_plugin target) + set_target_properties(${target} PROPERTIES + AUTOMOC ON + AUTOUIC ON + AUTORCC ON + CXX_STANDARD 23 + CXX_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON + ) + + # Keep bare uibase headers after system headers to avoid strings.h collisions. + target_compile_options(${target} PRIVATE + "-idirafter" "${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" + ) + set_property(TARGET ${target} APPEND PROPERTY AUTOMOC_MOC_OPTIONS + "-I${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" + ) +endfunction() + +function(mo2_default_source_group) + # no-op for now +endfunction() + +function(mo2_install_plugin target) + install(TARGETS ${target} + LIBRARY DESTINATION plugins + ) +endfunction() + +add_subdirectory(src/gamebryo) +add_subdirectory(src/creation) + +# Build all ported game plugins. +add_subdirectory(src/games/skyrimse) +add_subdirectory(src/games/skyrim) +add_subdirectory(src/games/skyrimvr) +add_subdirectory(src/games/fallout4) +add_subdirectory(src/games/fallout4vr) +add_subdirectory(src/games/fallout3) +add_subdirectory(src/games/falloutnv) +add_subdirectory(src/games/fallout76) +add_subdirectory(src/games/oblivion) +add_subdirectory(src/games/morrowind) +add_subdirectory(src/games/nehrim) +add_subdirectory(src/games/enderal) +add_subdirectory(src/games/enderalse) +add_subdirectory(src/games/starfield) +add_subdirectory(src/games/ttw) +add_subdirectory(src/games/fallout4london) diff --git a/libs/game_bethesda/CMakePresets.json b/libs/game_bethesda/CMakePresets.json new file mode 100644 index 0000000..cc009b3 --- /dev/null +++ b/libs/game_bethesda/CMakePresets.json @@ -0,0 +1,57 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/game_bethesda/src/creation/CMakeLists.txt b/libs/game_bethesda/src/creation/CMakeLists.txt new file mode 100644 index 0000000..9aa23d8 --- /dev/null +++ b/libs/game_bethesda/src/creation/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Qt6 COMPONENTS Widgets REQUIRED) + +# Try cmake config first, fall back to pkg-config +find_package(lz4 CONFIG QUIET) +if(NOT lz4_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LZ4 REQUIRED IMPORTED_TARGET liblz4) +endif() +# lz4 1.10+ exports LZ4::lz4, older versions export lz4::lz4 +if(TARGET LZ4::lz4 AND NOT TARGET lz4::lz4) + add_library(lz4::lz4 ALIAS LZ4::lz4) +endif() + +set(CMAKE_AUTOMOC ON) + +add_library(game_creation STATIC + creationgameplugins.h + creationgameplugins.cpp +) + +target_link_libraries(game_creation + PUBLIC game_gamebryo + PRIVATE $,lz4::lz4,PkgConfig::LZ4> +) + +target_include_directories(game_creation PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# Keep uibase bare includes after system headers to avoid strings.h collision. +target_compile_options(game_creation PRIVATE + "-idirafter" "${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" +) diff --git a/libs/game_bethesda/src/creation/creationgameplugins.cpp b/libs/game_bethesda/src/creation/creationgameplugins.cpp new file mode 100644 index 0000000..f5cef62 --- /dev/null +++ b/libs/game_bethesda/src/creation/creationgameplugins.cpp @@ -0,0 +1,183 @@ +#include "creationgameplugins.h" +#include +#include +#include +#include + +#include +#include +#include +#include + +using MOBase::IOrganizer; +using MOBase::IPluginGame; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +CreationGamePlugins::CreationGamePlugins(IOrganizer* organizer) + : GamebryoGamePlugins(organizer) +{} + +QStringList CreationGamePlugins::getLoadOrder() +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (loadOrderIsNew || !pluginsIsNew) { + return readLoadOrderList(m_Organizer->pluginList(), loadOrderPath); + } else { + return readPluginList(m_Organizer->pluginList()); + } +} + +void CreationGamePlugins::writePluginList(const IPluginList* pluginList, + const QString& filePath) +{ + SafeWriteFile file(filePath); + + QStringEncoder encoder(QStringConverter::Encoding::System); + + file->resize(0); + + file->write( + encoder.encode("# This file was automatically generated by Mod Organizer.\r\n")); + + bool invalidFileNames = false; + int writtenCount = 0; + + QStringList plugins = pluginList->pluginNames(); + std::sort(plugins.begin(), plugins.end(), + [pluginList](const QString& lhs, const QString& rhs) { + return pluginList->priority(lhs) < pluginList->priority(rhs); + }); + + QStringList PrimaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList DLCPlugins = organizer()->managedGame()->DLCPlugins(); + QSet ManagedMods = + QSet(PrimaryPlugins.begin(), PrimaryPlugins.end()); + QSet DLCSet = QSet(DLCPlugins.begin(), DLCPlugins.end()); + ManagedMods.subtract(DLCSet); + PrimaryPlugins.append(QList(ManagedMods.begin(), ManagedMods.end())); + + // TODO: do not write plugins in OFFICIAL_FILES container + for (const QString& pluginName : plugins) { + if (!PrimaryPlugins.contains(pluginName, Qt::CaseInsensitive)) { + if (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE) { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write("*"); + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } else { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } + } + } + + if (invalidFileNames) { + reportError(QObject::tr("Some of your plugins have invalid names! These " + "plugins can not be loaded by the game. Please see " + "mo_interface.log for a list of affected plugins " + "and rename them.")); + } + + file->commit(); +} + +QStringList CreationGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + const auto plugins = pluginList->pluginNames(); + const auto primaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList loadOrder(primaryPlugins); + + for (const QString& pluginName : loadOrder) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + qWarning("%s not found", qUtf8Printable(filePath)); + return loadOrder; + } + ON_BLOCK_EXIT([&]() { + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + qWarning("%s empty", qUtf8Printable(filePath)); + return loadOrder; + } + + QStringList pluginsFound; + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + pluginName = QStringEncoder(QStringConverter::Encoding::System) + .encode(line.trimmed().constData()); + } + if (!primaryPlugins.contains(pluginName, Qt::CaseInsensitive)) { + if (pluginName.startsWith('*')) { + pluginName.remove(0, 1); + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + pluginsFound.append(pluginName); + if (!loadOrder.contains(pluginName, Qt::CaseInsensitive)) { + loadOrder.append(pluginName); + } + } + } else { + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + pluginsFound.append(pluginName); + if (!loadOrder.contains(pluginName, Qt::CaseInsensitive)) { + loadOrder.append(pluginName); + } + } + } + } else { + pluginName.remove(0, 1); + pluginsFound.append(pluginName); + } + } + + file.close(); + + // set all plugins not found inactive + for (const auto& pluginName : plugins) { + if (!pluginsFound.contains(pluginName, Qt::CaseInsensitive)) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return loadOrder; +} + +bool CreationGamePlugins::lightPluginsAreSupported() +{ + return true; +} diff --git a/libs/game_bethesda/src/creation/creationgameplugins.h b/libs/game_bethesda/src/creation/creationgameplugins.h new file mode 100644 index 0000000..f7afd80 --- /dev/null +++ b/libs/game_bethesda/src/creation/creationgameplugins.h @@ -0,0 +1,22 @@ +#ifndef CREATIONGAMEPLUGINS_H +#define CREATIONGAMEPLUGINS_H + +#include +#include +#include +#include + +class CreationGamePlugins : public GamebryoGamePlugins +{ +public: + CreationGamePlugins(MOBase::IOrganizer* organizer); + +protected: + virtual void writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) override; + virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override; + virtual QStringList getLoadOrder() override; + virtual bool lightPluginsAreSupported() override; +}; + +#endif // CREATIONGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/creation/game_creation_en.ts b/libs/game_bethesda/src/creation/game_creation_en.ts new file mode 100644 index 0000000..f217448 --- /dev/null +++ b/libs/game_bethesda/src/creation/game_creation_en.ts @@ -0,0 +1,12 @@ + + + + + QObject + + + Some of your plugins have invalid names! These plugins can not be loaded by the game. Please see mo_interface.log for a list of affected plugins and rename them. + + + + diff --git a/libs/game_bethesda/src/gamebryo/CMakeLists.txt b/libs/game_bethesda/src/gamebryo/CMakeLists.txt new file mode 100644 index 0000000..bbc47ae --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/CMakeLists.txt @@ -0,0 +1,78 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Qt6 COMPONENTS Widgets REQUIRED) +find_package(ZLIB REQUIRED) + +# Try cmake config first, fall back to pkg-config +find_package(lz4 CONFIG QUIET) +if(NOT lz4_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LZ4 REQUIRED IMPORTED_TARGET liblz4) +endif() +# lz4 1.10+ exports LZ4::lz4, older versions export lz4::lz4 +if(TARGET LZ4::lz4 AND NOT TARGET lz4::lz4) + add_library(lz4::lz4 ALIAS LZ4::lz4) +endif() + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) + +add_library(game_gamebryo STATIC + dummybsa.cpp + dummybsa.h + gamebryobsainvalidation.cpp + gamebryobsainvalidation.h + gamebryodataarchives.cpp + gamebryodataarchives.h + gamebryogameplugins.cpp + gamebryogameplugins.h + gamebryolocalsavegames.cpp + gamebryolocalsavegames.h + gamebryomoddatachecker.cpp + gamebryomoddatachecker.h + gamebryomoddatacontent.cpp + gamebryomoddatacontent.h + gamebryosavegame.cpp + gamebryosavegame.h + gamebryosavegameinfo.cpp + gamebryosavegameinfo.h + gamebryosavegameinfowidget.cpp + gamebryosavegameinfowidget.h + gamebryosavegameinfowidget.ui + gamebryoscriptextender.cpp + gamebryoscriptextender.h + gamebryounmanagedmods.cpp + gamebryounmanagedmods.h + gamegamebryo.cpp + gamegamebryo.h + vdf_parser.h +) + +target_link_libraries(game_gamebryo + PUBLIC + uibase + game_features + Qt6::Widgets + PRIVATE + ZLIB::ZLIB + $,lz4::lz4,PkgConfig::LZ4> +) + +# NaK FFI for game detection (replaces Windows registry) +if(TARGET mo2::nak_ffi) + target_link_libraries(game_gamebryo PRIVATE mo2::nak_ffi) + target_compile_definitions(game_gamebryo PRIVATE HAS_NAK_FFI) +endif() + +target_include_directories(game_gamebryo PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# Keep uibase bare includes after system headers to avoid strings.h collision. +target_compile_options(game_gamebryo PRIVATE + "-idirafter" "${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" +) + +# AUTOMOC does not reliably honor -idirafter, so provide the bare uibase include +# path explicitly for moc parsing of Q_INTERFACES declarations. +set_property(TARGET game_gamebryo APPEND PROPERTY AUTOMOC_MOC_OPTIONS + "-I${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" +) diff --git a/libs/game_bethesda/src/gamebryo/dummybsa.cpp b/libs/game_bethesda/src/gamebryo/dummybsa.cpp new file mode 100644 index 0000000..7b0f877 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/dummybsa.cpp @@ -0,0 +1,198 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "dummybsa.h" +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#ifndef MAX_PATH +#define MAX_PATH 260 +#endif +#endif + +static void writeUlong(unsigned char* buffer, int offset, unsigned long value) +{ + union + { + unsigned long ulValue; + unsigned char cValue[4]; + }; + ulValue = value; + memcpy(buffer + offset, cValue, 4); +} + +static void writeUlonglong(unsigned char* buffer, int offset, unsigned long long value) +{ + union + { + unsigned long long ullValue; + unsigned char cValue[8]; + }; + ullValue = value; + memcpy(buffer + offset, cValue, 8); +} + +static unsigned long genHashInt(const unsigned char* pos, const unsigned char* end) +{ + unsigned long hash = 0; + for (; pos < end; ++pos) { + hash *= 0x1003f; + hash += *pos; + } + return hash; +} + +static unsigned long long genHash(const char* fileName) +{ + char fileNameLower[MAX_PATH + 1]; + int i = 0; + for (; i < MAX_PATH && fileName[i] != '\0'; ++i) { + fileNameLower[i] = static_cast(tolower(fileName[i])); + if (fileNameLower[i] == '\\') { + fileNameLower[i] = '/'; + } + } + fileNameLower[i] = '\0'; + + unsigned char* fileNameLowerU = reinterpret_cast(fileNameLower); + + char* ext = strrchr(fileNameLower, '.'); + if (ext == nullptr) { + ext = fileNameLower + strlen(fileNameLower); + } + unsigned char* extU = reinterpret_cast(ext); + + int length = ext - fileNameLower; + + unsigned long long hash = 0ULL; + + if (length > 0) { + hash = *(extU - 1) | ((length > 2 ? *(ext - 2) : 0) << 8) | (length << 16) | + (fileNameLowerU[0] << 24); + } + + if (strlen(ext) > 0) { + if (strcmp(ext + 1, "kf") == 0) { + hash |= 0x80; + } else if (strcmp(ext + 1, "nif") == 0) { + hash |= 0x8000; + } else if (strcmp(ext + 1, "dds") == 0) { + hash |= 0x8080; + } else if (strcmp(ext + 1, "wav") == 0) { + hash |= 0x80000000; + } + + unsigned long long temp = + static_cast(genHashInt(fileNameLowerU + 1, extU - 2)); + temp += static_cast(genHashInt(extU, extU + strlen(ext))); + + hash |= (temp & 0xFFFFFFFF) << 32; + } + return hash; +} + +DummyBSA::DummyBSA(unsigned long bsaVersion) + : m_Version(bsaVersion), m_FolderName(""), m_FileName("dummy.dds"), + m_TotalFileNameLength(0) +{} + +void DummyBSA::writeHeader(QFile& file) +{ + unsigned char header[] = { + 'B', 'S', 'A', '\0', // magic string + 0xDE, 0xAD, 0xBE, 0xEF, // version - insert later + 0x24, 0x00, 0x00, 0x00, // offset to folder recors. header size is static + 0xDE, 0xAD, 0xBE, 0xEF, // archive flags - insert later + 0x01, 0x00, 0x00, 0x00, // folder count + 0x01, 0x00, 0x00, 0x00, // file count + 0xDE, 0xAD, 0xBE, 0xEF, // total folder names length - insert later + 0xDE, 0xAD, 0xBE, 0xEF, // total file names length - insert later + 0xDE, 0xAD, 0xBE, 0xEF // file flags - insert later + }; + + writeUlong(header, 4, m_Version); + writeUlong(header, 12, 0x01 | 0x02); // has directories and has files. + writeUlong(header, 24, + static_cast(m_FolderName.length()) + + 1); // empty folder name + writeUlong(header, 28, m_TotalFileNameLength); // single character file name + + writeUlong(header, 32, 2); // has dds + + file.write(reinterpret_cast(header), sizeof(header)); +} + +void DummyBSA::writeFolderRecord(QFile& file, const std::string& folderName) +{ + unsigned char folderRecord[] = { + 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, // folder hash + 0x01, 0x00, 0x00, 0x00, // file count + 0xDE, 0xAD, 0xBE, 0xEF, // offset to folder name + }; + // we'd usually have to sort folders be the hash value generated here + writeUlonglong(folderRecord, 0, genHash(folderName.c_str())); + writeUlong(folderRecord, 12, + 0x34 + m_TotalFileNameLength); // TODO: this should be calculated properly + + file.write(reinterpret_cast(folderRecord), sizeof(folderRecord)); +} + +void DummyBSA::writeFileRecord(QFile& file, const std::string& fileName) +{ + unsigned char fileRecord[] = { + 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, // file name hash + 0xDE, 0xAD, 0xBE, 0xEF, // size + 0xDE, 0xAD, 0xBE, 0xEF, // offset to file data + }; + + // we'd usually have to sort files by the value generated here + writeUlonglong(fileRecord, 0, genHash(fileName.c_str())); + writeUlong(fileRecord, 8, 0); + writeUlong( + fileRecord, 12, + 0x44 + static_cast(fileName.length() + 1) + + 4); // after this record we expect the filename and 4 bytes of file size + + file.write(reinterpret_cast(fileRecord), sizeof(fileRecord)); +} + +void DummyBSA::writeFileRecordBlocks(QFile& file, const std::string& folderName) +{ + file.write(folderName.c_str(), folderName.length() + 1); + + writeFileRecord(file, m_FileName); +} + +void DummyBSA::write(const QString& fileName) +{ + QFile file(fileName); + file.open(QIODevice::WriteOnly); + + m_TotalFileNameLength = static_cast(m_FileName.length() + 1); + + writeHeader(file); + writeFolderRecord(file, m_FolderName); + writeFileRecordBlocks(file, m_FolderName); + file.write(m_FileName.c_str(), m_FileName.length() + 1); + char fileSize[] = {0x00, 0x00, 0x00, 0x00}; + file.write(fileSize, sizeof(fileSize)); + file.close(); +} diff --git a/libs/game_bethesda/src/gamebryo/dummybsa.h b/libs/game_bethesda/src/gamebryo/dummybsa.h new file mode 100644 index 0000000..8ea071b --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/dummybsa.h @@ -0,0 +1,59 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef DUMMYBSA_H +#define DUMMYBSA_H + +#include +#include + +/** + * @brief Class for creating a dummy bsa used for archive invalidation + **/ +class DummyBSA +{ + +public: + /** + * @brief constructor + * + **/ + DummyBSA(unsigned long bsaVersion); + + /** + * @brief write to the specified file + * + * @param fileName name of the file to write to + **/ + void write(const QString& fileName); + +private: + void writeHeader(QFile& file); + void writeFolderRecord(QFile& file, const std::string& folderName); + void writeFileRecord(QFile& file, const std::string& fileName); + void writeFileRecordBlocks(QFile& file, const std::string& folderName); + +private: + unsigned long m_Version; + std::string m_FolderName; + std::string m_FileName; + unsigned long m_TotalFileNameLength; +}; + +#endif // DUMMYBSA_H diff --git a/libs/game_bethesda/src/gamebryo/game_gamebryo_en.ts b/libs/game_bethesda/src/gamebryo/game_gamebryo_en.ts new file mode 100644 index 0000000..add224d --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/game_gamebryo_en.ts @@ -0,0 +1,171 @@ + + + + + GamebryoModDataContent + + + Plugins (ESP/ESM/ESL) + + + + + Optional Plugins + + + + + Interface + + + + + Meshes + + + + + Bethesda Archive + + + + + Scripts (Papyrus) + + + + + Script Extender Plugin + + + + + Script Extender Files + + + + + SkyProc Patcher + + + + + Sound or Music + + + + + Textures + + + + + MCM Configuration + + + + + INI Files + + + + + FaceGen Data + + + + + ModGroup Files + + + + + GamebryoSaveGameInfoWidget + + + Save # + + + + + Character + + + + + Level + + + + + Location + + + + + Date + + + + + Has Script Extender Data + + + + + Missing ESPs + + + + + + + None + + + + + Missing ESHs + + + + + Missing ESLs + + + + + QObject + + + + Some of your plugins have invalid names! These plugins can not be loaded by the game. Please see mo_interface.log for a list of affected plugins and rename them. + + + + + %1, #%2, Level %3, %4 + + + + + failed to open %1 + + + + + wrong file format - expected %1 got '%2' for %3 + + + + + failed to query registry path (preflight): %1 + + + + + failed to query registry path (read): %1 + + + + diff --git a/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.cpp b/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.cpp new file mode 100644 index 0000000..6170305 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.cpp @@ -0,0 +1,165 @@ +#include "gamebryobsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include +#include + +#include +#include +#include + +#include "gamegamebryo.h" + +GamebryoBSAInvalidation::GamebryoBSAInvalidation(MOBase::DataArchives* dataArchives, + const QString& iniFilename, + MOBase::IPluginGame const* game) + : m_DataArchives(dataArchives), m_IniFileName(iniFilename), m_Game(game) +{} + +bool GamebryoBSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + static QStringList invalidation{invalidationBSAName()}; + + for (const QString& file : invalidation) { + if (file.compare(bsaName, Qt::CaseInsensitive) == 0) { + return true; + } + } + return false; +} + +void GamebryoBSAInvalidation::deactivate(MOBase::IProfile* profile) +{ + prepareProfile(profile); +} + +void GamebryoBSAInvalidation::activate(MOBase::IProfile* profile) +{ + prepareProfile(profile); +} + +// Case-insensitive file resolution (same logic as gamegamebryo.cpp). +static QString resolveIniPathCaseInsensitive(const QString& filePath) +{ +#ifdef _WIN32 + return filePath; +#else + if (QFileInfo(filePath).exists()) { + return filePath; + } + QFileInfo info(filePath); + QDir dir(info.path()); + if (!dir.exists()) { + return filePath; + } + const QString target = info.fileName(); + const QStringList entries = dir.entryList(QDir::Files | QDir::Hidden); + for (const QString& entry : entries) { + if (entry.compare(target, Qt::CaseInsensitive) == 0) { + return dir.absoluteFilePath(entry); + } + } + return filePath; +#endif +} + +bool GamebryoBSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + + // Ensure the target INI has adequate content before writing to it. + // On Linux, the game launcher may not work, leaving INI files missing + // or as empty stubs. Seed from the game's default INI if needed. + const auto* gamebryo = dynamic_cast(m_Game); + if (gamebryo) { + const_cast(gamebryo)->ensureIniFilesExist(basePath); + } + + // Resolve case-insensitively (e.g., fallout.ini vs Fallout.ini on Linux) + QString iniFilePath = resolveIniPathCaseInsensitive(basePath + "/" + m_IniFileName); + + // write bInvalidateOlderFiles = 1, if needed + QString setting = + GameGamebryo::readIniValue(iniFilePath, "Archive", "bInvalidateOlderFiles", "0"); + if (setting.toLong() != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "bInvalidateOlderFiles", "1", + iniFilePath)) { + qWarning("failed to activate BSA invalidation in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + + if (profile->invalidationActive(nullptr)) { + + // add the dummy bsa to the archive string, if needed + QStringList archives = m_DataArchives->archives(profile); + bool bsaInstalled = false; + for (const QString& archive : archives) { + if (isInvalidationBSA(archive)) { + bsaInstalled = true; + break; + } + } + if (!bsaInstalled) { + m_DataArchives->addArchive(profile, 0, invalidationBSAName()); + dirty = true; + } + + // create the dummy bsa if necessary + QString bsaFile = m_Game->dataDirectory().absoluteFilePath(invalidationBSAName()); + if (!QFile::exists(bsaFile)) { + DummyBSA bsa(bsaVersion()); + bsa.write(bsaFile); + dirty = true; + } + + // write SInvalidationFile = "", if needed + QString sInvalidation = GameGamebryo::readIniValue( + iniFilePath, "Archive", "SInvalidationFile", "ArchiveInvalidation.txt"); + if (sInvalidation != "") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "SInvalidationFile", "", iniFilePath)) { + qWarning("failed to activate BSA invalidation in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } else { + + // remove the dummy bsa from the archive string, if needed + QStringList archivesBefore = m_DataArchives->archives(profile); + for (const QString& archive : archivesBefore) { + if (isInvalidationBSA(archive)) { + m_DataArchives->removeArchive(profile, archive); + dirty = true; + } + } + + // delete the dummy bsa, if needed + QString bsaFile = m_Game->dataDirectory().absoluteFilePath(invalidationBSAName()); + if (QFile::exists(bsaFile)) { + MOBase::shellDeleteQuiet(bsaFile); + dirty = true; + } + + // write SInvalidationFile = "ArchiveInvalidation.txt", if needed + QString sInvalidation2 = + GameGamebryo::readIniValue(iniFilePath, "Archive", "SInvalidationFile", ""); + if (sInvalidation2 != "ArchiveInvalidation.txt") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "SInvalidationFile", + "ArchiveInvalidation.txt", iniFilePath)) { + qWarning("failed to activate BSA invalidation in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.h b/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.h new file mode 100644 index 0000000..efd3d7b --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.h @@ -0,0 +1,36 @@ +#ifndef GAMEBRYOBSAINVALIDATION_H +#define GAMEBRYOBSAINVALIDATION_H + +#include +#include +#include +#include + +namespace MOBase +{ +class IPluginGame; +} + +class GamebryoBSAInvalidation : public MOBase::BSAInvalidation +{ +public: + GamebryoBSAInvalidation(MOBase::DataArchives* dataArchives, + const QString& iniFilename, MOBase::IPluginGame const* game); + + virtual bool isInvalidationBSA(const QString& bsaName) override; + virtual void deactivate(MOBase::IProfile* profile) override; + virtual void activate(MOBase::IProfile* profile) override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +private: + virtual QString invalidationBSAName() const = 0; + virtual unsigned long + bsaVersion() const = 0; // 0x67 for oblivion, 0x68 for everything else + +private: + MOBase::DataArchives* m_DataArchives; + QString m_IniFileName; + MOBase::IPluginGame const* m_Game; +}; + +#endif // GAMEBRYOBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/gamebryo/gamebryodataarchives.cpp b/libs/game_bethesda/src/gamebryo/gamebryodataarchives.cpp new file mode 100644 index 0000000..11e5408 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryodataarchives.cpp @@ -0,0 +1,68 @@ +#include "gamebryodataarchives.h" + +#include +#include + +#include "gamegamebryo.h" + +GamebryoDataArchives::GamebryoDataArchives(const GameGamebryo* game) : m_Game{game} {} + +QDir GamebryoDataArchives::gameDirectory() const +{ + return QDir(m_Game->gameDirectory()).absolutePath(); +} + +QDir GamebryoDataArchives::localGameDirectory() const +{ + return QDir(m_Game->myGamesPath()).absolutePath(); +} + +QStringList GamebryoDataArchives::getArchivesFromKey(const QString& iniFile, + const QString& key, + const int size) const +{ + QStringList result; + + QString value = GameGamebryo::readIniValue(iniFile, "Archive", key, ""); + if (!value.isEmpty()) { + result.append(value.split(',')); + } + + for (int i = 0; i < result.count(); ++i) { + result[i] = result[i].trimmed(); + } + return result; +} + +void GamebryoDataArchives::setArchivesToKey(const QString& iniFile, const QString& key, + const QString& value) +{ + if (!MOBase::WriteRegistryValue("Archive", key, value, iniFile)) { + qWarning("failed to set archives in \"%s\"", qUtf8Printable(iniFile)); + } +} + +void GamebryoDataArchives::addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) +{ + QStringList current = archives(profile); + if (current.contains(archiveName, Qt::CaseInsensitive)) { + return; + } + + current.insert(index != INT_MAX ? index : current.size(), archiveName); + + writeArchiveList(profile, current); +} + +void GamebryoDataArchives::removeArchive(MOBase::IProfile* profile, + const QString& archiveName) +{ + QStringList current = archives(profile); + if (!current.contains(archiveName, Qt::CaseInsensitive)) { + return; + } + current.removeAll(archiveName); + + writeArchiveList(profile, current); +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryodataarchives.h b/libs/game_bethesda/src/gamebryo/gamebryodataarchives.h new file mode 100644 index 0000000..13f4130 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryodataarchives.h @@ -0,0 +1,37 @@ +#ifndef GAMEBRYODATAARCHIVES_H +#define GAMEBRYODATAARCHIVES_H + +#include + +#include "dataarchives.h" + +class GameGamebryo; + +class GamebryoDataArchives : public MOBase::DataArchives +{ + +public: + GamebryoDataArchives(const GameGamebryo* game); + + virtual void addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) override; + virtual void removeArchive(MOBase::IProfile* profile, + const QString& archiveName) override; + +protected: + QDir gameDirectory() const; + QDir localGameDirectory() const; + + QStringList getArchivesFromKey(const QString& iniFile, const QString& key, + int size = 256) const; + void setArchivesToKey(const QString& iniFile, const QString& key, + const QString& value); + +private: + const GameGamebryo* m_Game; + + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) = 0; +}; + +#endif // GAMEBRYODATAARCHIVES_H diff --git a/libs/game_bethesda/src/gamebryo/gamebryogameplugins.cpp b/libs/game_bethesda/src/gamebryo/gamebryogameplugins.cpp new file mode 100644 index 0000000..1accc4f --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryogameplugins.cpp @@ -0,0 +1,246 @@ +#include "gamebryogameplugins.h" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using MOBase::IOrganizer; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +GamebryoGamePlugins::GamebryoGamePlugins(IOrganizer* organizer) : m_Organizer(organizer) +{} + +void GamebryoGamePlugins::writePluginLists(const IPluginList* pluginList) +{ + if (!m_LastRead.isValid()) { + // attempt to write uninitialized plugin lists + return; + } + + writePluginList(pluginList, m_Organizer->profile()->absolutePath() + "/plugins.txt"); + writeLoadOrderList(pluginList, + m_Organizer->profile()->absolutePath() + "/loadorder.txt"); + + m_LastRead = QDateTime::currentDateTime(); +} + +void GamebryoGamePlugins::readPluginLists(MOBase::IPluginList* pluginList) +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (loadOrderIsNew || !pluginsIsNew) { + // read both files if they are both new or both older than the last read + QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); + pluginList->setLoadOrder(loadOrder); + readPluginList(pluginList); + } else { + // If the plugins is new but not loadorder, we must reparse the load order from the + // plugin files + QStringList loadOrder = readPluginList(pluginList); + pluginList->setLoadOrder(loadOrder); + } + + m_LastRead = QDateTime::currentDateTime(); +} + +QStringList GamebryoGamePlugins::getLoadOrder() +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (loadOrderIsNew || !pluginsIsNew) { + return readLoadOrderList(m_Organizer->pluginList(), loadOrderPath); + } else { + return readPluginList(m_Organizer->pluginList()); + } +} + +void GamebryoGamePlugins::writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) +{ + return writeList(pluginList, filePath, false); +} + +void GamebryoGamePlugins::writeLoadOrderList(const MOBase::IPluginList* pluginList, + const QString& filePath) +{ + return writeList(pluginList, filePath, true); +} + +void GamebryoGamePlugins::writeList(const IPluginList* pluginList, + const QString& filePath, bool loadOrder) +{ + SafeWriteFile file(filePath); + + QStringEncoder encoder = loadOrder + ? QStringEncoder(QStringConverter::Encoding::Utf8) + : QStringEncoder(QStringConverter::Encoding::System); + + file->resize(0); + + file->write( + encoder.encode("# This file was automatically generated by Mod Organizer.\r\n")); + + bool invalidFileNames = false; + int writtenCount = 0; + + QStringList plugins = pluginList->pluginNames(); + std::sort(plugins.begin(), plugins.end(), + [pluginList](const QString& lhs, const QString& rhs) { + return pluginList->priority(lhs) < pluginList->priority(rhs); + }); + + for (const QString& pluginName : plugins) { + if (loadOrder || (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE)) { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } + } + + if (invalidFileNames) { + reportError(QObject::tr("Some of your plugins have invalid names! These " + "plugins can not be loaded by the game. Please see " + "mo_interface.log for a list of affected plugins " + "and rename them.")); + } + + if (writtenCount == 0) { + qWarning("plugin list would be empty, this is almost certainly wrong. Not " + "saving."); + } else { + file->commit(); + } +} + +QStringList GamebryoGamePlugins::readLoadOrderList(MOBase::IPluginList* pluginList, + const QString& filePath) +{ + QStringList pluginNames = organizer()->managedGame()->primaryPlugins(); + + std::set pluginLookup; + for (auto&& name : pluginNames) { + pluginLookup.insert(name.toLower()); + } + + const auto b = MOBase::forEachLineInFile(filePath, [&](QString s) { + if (!pluginLookup.contains(s.toLower())) { + pluginLookup.insert(s); + pluginNames.push_back(std::move(s)); + } + }); + + if (!b) { + return readPluginList(pluginList); + } + + return pluginNames; +} + +QStringList GamebryoGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + QStringList primary = organizer()->managedGame()->primaryPlugins(); + for (const QString& pluginName : primary) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + QStringList plugins = pluginList->pluginNames(); + QStringList pluginsClone(plugins); + // Do not sort the primary plugins. Their load order should be locked as defined in + // "primaryPlugins". + for (const auto& plugin : pluginsClone) { + if (primary.contains(plugin, Qt::CaseInsensitive)) + plugins.removeAll(plugin); + } + + // Always use filetime loadorder to get the actual load order + QHash modificationTimes; + modificationTimes.reserve(plugins.size()); + for (const auto& pluginName : plugins) { + MOBase::IModInterface* mod = + organizer()->modList()->getMod(pluginList->origin(pluginName)); + QDir dir = mod ? mod->absolutePath() : organizer()->managedGame()->dataDirectory(); + QString absPath = dir.absoluteFilePath(pluginName); + modificationTimes.insert(pluginName, QFileInfo(absPath).lastModified()); + } + + std::ranges::sort( + plugins, [&modificationTimes](const QString& lhs, const QString& rhs) { + return modificationTimes.value(lhs) < modificationTimes.value(rhs); + }); + + // Determine plugin active state by the plugins.txt file. + bool pluginsTxtExists = true; + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + pluginsTxtExists = false; + } + ON_BLOCK_EXIT([&]() { + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + pluginsTxtExists = false; + } + + QStringList activePlugins; + if (pluginsTxtExists) { + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + QStringEncoder encoder(QStringConverter::Encoding::System); + pluginName = encoder.encode(line.trimmed().constData()); + } + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + activePlugins.push_back(pluginName); + } + } + + for (const auto& pluginName : plugins) { + if (!activePlugins.contains(pluginName, Qt::CaseInsensitive)) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + } else { + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return primary + plugins; +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryogameplugins.h b/libs/game_bethesda/src/gamebryo/gamebryogameplugins.h new file mode 100644 index 0000000..ca70c75 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryogameplugins.h @@ -0,0 +1,38 @@ +#ifndef GAMEBRYOGAMEPLUGINS_H +#define GAMEBRYOGAMEPLUGINS_H + +#include +#include +#include +#include + +class GamebryoGamePlugins : public MOBase::GamePlugins +{ +public: + GamebryoGamePlugins(MOBase::IOrganizer* organizer); + + virtual void writePluginLists(const MOBase::IPluginList* pluginList) override; + virtual void readPluginLists(MOBase::IPluginList* pluginList) override; + virtual QStringList getLoadOrder() override; + +protected: + MOBase::IOrganizer* organizer() const { return m_Organizer; } + + virtual void writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath); + virtual void writeLoadOrderList(const MOBase::IPluginList* pluginList, + const QString& filePath); + virtual QStringList readLoadOrderList(MOBase::IPluginList* pluginList, + const QString& filePath); + virtual QStringList readPluginList(MOBase::IPluginList* pluginList); + +protected: + MOBase::IOrganizer* m_Organizer; + QDateTime m_LastRead; + +private: + void writeList(const MOBase::IPluginList* pluginList, const QString& filePath, + bool loadOrder); +}; + +#endif // GAMEBRYOGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp new file mode 100644 index 0000000..e4df0ea --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp @@ -0,0 +1,138 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "gamebryolocalsavegames.h" +#include "registry.h" +#include +#include +#include +#include + +#include + +#include "gamegamebryo.h" + +// Helper to remove a key from a Bethesda-style INI file +static void removeIniValue(const QString& iniFile, const QString& section, + const QString& key) +{ + QSettings settings(iniFile, QSettings::IniFormat); + settings.beginGroup(section); + settings.remove(key); + settings.endGroup(); + settings.sync(); +} + +GamebryoLocalSavegames::GamebryoLocalSavegames(const GameGamebryo* game, + const QString& iniFileName) + : m_Game{game}, m_IniFileName(iniFileName) +{} + +MappingType GamebryoLocalSavegames::mappings(const QDir& profileSaveDir) const +{ + return {{profileSaveDir.absolutePath(), localSavesDirectory().absolutePath(), true, + true}}; +} + +QString GamebryoLocalSavegames::localSavesDummy() const +{ + return "__MO_Saves\\"; +} + +QDir GamebryoLocalSavegames::localSavesDirectory() const +{ + return QDir(m_Game->myGamesPath()).absoluteFilePath(localSavesDummy()); +} + +QDir GamebryoLocalSavegames::localGameDirectory() const +{ + return QDir(m_Game->myGamesPath()).absolutePath(); +} + +bool GamebryoLocalSavegames::prepareProfile(MOBase::IProfile* profile) +{ + bool enable = profile->localSavesEnabled(); + + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : localGameDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + QString saveIni = profile->absolutePath() + "/" + "savepath.ini"; + + // Get the current sLocalSavePath + QString currentPath = + GameGamebryo::readIniValue(iniFilePath, "General", "sLocalSavePath", "SKIP_ME"); + bool alreadyEnabled = (currentPath == localSavesDummy()); + + // Get the current bUseMyGamesDirectory + QString currentMyGames = GameGamebryo::readIniValue( + iniFilePath, "General", "bUseMyGamesDirectory", "SKIP_ME"); + + // Create the __MO_Saves directory if local saves are enabled and it doesn't exist + if (enable) { + QDir saves = localSavesDirectory(); + if (!saves.exists()) { + saves.mkdir("."); + } + } + + // Set the path to __MO_Saves if it's not already + if (enable && !alreadyEnabled) { + // If the path is not blank, save it to savepath.ini + if (currentPath != "SKIP_ME") { + MOBase::WriteRegistryValue("General", "sLocalSavePath", currentPath, saveIni); + } + if (currentMyGames != "SKIP_ME") { + MOBase::WriteRegistryValue("General", "bUseMyGamesDirectory", currentMyGames, + saveIni); + } + MOBase::WriteRegistryValue("General", "sLocalSavePath", localSavesDummy(), + iniFilePath); + MOBase::WriteRegistryValue("General", "bUseMyGamesDirectory", "1", iniFilePath); + } + + // Get rid of the local saves setting if it's still there + if (!enable && alreadyEnabled) { + // If savepath.ini exists, use it and delete it + if (QFile::exists(saveIni)) { + QString savedPath = + GameGamebryo::readIniValue(saveIni, "General", "sLocalSavePath", "DELETE_ME"); + QString savedMyGames = GameGamebryo::readIniValue( + saveIni, "General", "bUseMyGamesDirectory", "DELETE_ME"); + if (savedPath != "DELETE_ME") { + MOBase::WriteRegistryValue("General", "sLocalSavePath", savedPath, iniFilePath); + } else { + removeIniValue(iniFilePath, "General", "sLocalSavePath"); + } + if (savedMyGames != "DELETE_ME") { + MOBase::WriteRegistryValue("General", "bUseMyGamesDirectory", savedMyGames, + iniFilePath); + } else { + removeIniValue(iniFilePath, "General", "bUseMyGamesDirectory"); + } + QFile::remove(saveIni); + } + // Otherwise just delete the setting + else { + removeIniValue(iniFilePath, "General", "sLocalSavePath"); + removeIniValue(iniFilePath, "General", "bUseMyGamesDirectory"); + } + } + + return enable != alreadyEnabled; +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.h b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.h new file mode 100644 index 0000000..e5f2a92 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.h @@ -0,0 +1,54 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef GAMEBRYOLOCALSAVEGAMES_H +#define GAMEBRYOLOCALSAVEGAMES_H + +#include + +#include +#include + +class GameGamebryo; + +class GamebryoLocalSavegames : public MOBase::LocalSavegames +{ + +public: + GamebryoLocalSavegames(const GameGamebryo* game, const QString& iniFileName); + + virtual MappingType mappings(const QDir& profileSaveDir) const override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +protected: + // return the path from the local game directory to the local saves folder + // + // this is virtual so game plugins for complete game overhauld (Enderal, Nehrim, etc.) + // can override it properly + // + virtual QString localSavesDummy() const; + + QDir localSavesDirectory() const; + QDir localGameDirectory() const; + +private: + const GameGamebryo* m_Game; + QString m_IniFileName; +}; + +#endif // GAMEBRYOLOCALSAVEGAMES_H diff --git a/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.cpp b/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.cpp new file mode 100644 index 0000000..b5bace6 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.cpp @@ -0,0 +1,76 @@ +#include + +#include "gamebryomoddatachecker.h" + +/** + * @return the list of possible folder names in data. + */ +auto GamebryoModDataChecker::possibleFolderNames() const -> const FileNameSet& +{ + static FileNameSet result{"fonts", + "interface", + "menus", + "meshes", + "music", + "scripts", + "shaders", + "sound", + "strings", + "textures", + "trees", + "video", + "facegen", + "materials", + "skse", + "obse", + "mwse", + "nvse", + "fose", + "f4se", + "distantlod", + "asi", + "SkyProc Patchers", + "Tools", + "MCM", + "icons", + "bookart", + "distantland", + "mits", + "splash", + "dllplugins", + "CalienteTools", + "NetScriptFramework", + "shadersfx"}; + return result; +} + +/** + * @return the extensions of possible files in data. + */ +auto GamebryoModDataChecker::possibleFileExtensions() const -> const FileNameSet& +{ + static FileNameSet result{"esp", "esm", "esl", "bsa", "ba2", "modgroups", "ini"}; + return result; +} + +GamebryoModDataChecker::GamebryoModDataChecker(const GameGamebryo* game) : m_Game(game) +{} + +GamebryoModDataChecker::CheckReturn GamebryoModDataChecker::dataLooksValid( + std::shared_ptr fileTree) const +{ + auto& folders = possibleFolderNames(); + auto& suffixes = possibleFileExtensions(); + for (auto entry : *fileTree) { + if (entry->isDir()) { + if (folders.count(entry->name()) > 0) { + return CheckReturn::VALID; + } + } else { + if (suffixes.count(entry->suffix()) > 0) { + return CheckReturn::VALID; + } + } + } + return CheckReturn::INVALID; +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.h b/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.h new file mode 100644 index 0000000..e0b0733 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.h @@ -0,0 +1,46 @@ +#ifndef GAMEBRYO_MODATACHECKER_H +#define GAMEBRYO_MODATACHECKER_H + +#include +#include + +class GameGamebryo; + +/** + * @brief ModDataChecker for GameBryo games that look at folder and files in the "data" + * directory. + * + * The default implementation is game-agnostic and uses the list of folders and file + * extensions that were used before the ModDataChecker feature was added. It is possible + * to inherit the class to provide custom list of folders or filenames. + */ +class GamebryoModDataChecker : public MOBase::ModDataChecker +{ +public: + /** + * @brief Construct a new mod-data checker for GameBryo games. + */ + GamebryoModDataChecker(const GameGamebryo* game); + + virtual CheckReturn + dataLooksValid(std::shared_ptr fileTree) const override; + +protected: + GameGamebryo const* const m_Game; + + using FileNameSet = std::set; + + const GameGamebryo* game() const { return m_Game; } + + /** + * @return the list of possible folder names in data. + */ + virtual const FileNameSet& possibleFolderNames() const; + + /** + * @return the extensions of possible files in data. + */ + virtual const FileNameSet& possibleFileExtensions() const; +}; + +#endif // GAMEBRYO_MODATACHECKER_H diff --git a/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.cpp b/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.cpp new file mode 100644 index 0000000..85b66b0 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.cpp @@ -0,0 +1,116 @@ +#include "gamebryomoddatacontent.h" + +#include +#include + +GamebryoModDataContent::GamebryoModDataContent( + MOBase::IGameFeatures const* gameFeatures) + : m_GameFeatures(gameFeatures), m_Enabled(CONTENT_MODGROUP + 1, true) +{} + +std::vector +GamebryoModDataContent::getAllContents() const +{ + static std::vector GAMEBRYO_CONTENTS{ + {CONTENT_PLUGIN, QT_TR_NOOP("Plugins (ESP/ESM/ESL)"), ":/MO/gui/content/plugin"}, + {CONTENT_OPTIONAL, QT_TR_NOOP("Optional Plugins"), "", true}, + {CONTENT_INTERFACE, QT_TR_NOOP("Interface"), ":/MO/gui/content/interface"}, + {CONTENT_MESH, QT_TR_NOOP("Meshes"), ":/MO/gui/content/mesh"}, + {CONTENT_BSA, QT_TR_NOOP("Bethesda Archive"), ":/MO/gui/content/bsa"}, + {CONTENT_SCRIPT, QT_TR_NOOP("Scripts (Papyrus)"), ":/MO/gui/content/script"}, + {CONTENT_SKSE, QT_TR_NOOP("Script Extender Plugin"), ":/MO/gui/content/skse"}, + {CONTENT_SKSE_FILES, QT_TR_NOOP("Script Extender Files"), "", true}, + {CONTENT_SKYPROC, QT_TR_NOOP("SkyProc Patcher"), ":/MO/gui/content/skyproc"}, + {CONTENT_SOUND, QT_TR_NOOP("Sound or Music"), ":/MO/gui/content/sound"}, + {CONTENT_TEXTURE, QT_TR_NOOP("Textures"), ":/MO/gui/content/texture"}, + {CONTENT_MCM, QT_TR_NOOP("MCM Configuration"), ":/MO/gui/content/menu"}, + {CONTENT_INI, QT_TR_NOOP("INI Files"), ":/MO/gui/content/inifile"}, + {CONTENT_FACEGEN, QT_TR_NOOP("FaceGen Data"), ":/MO/gui/content/facegen"}, + {CONTENT_MODGROUP, QT_TR_NOOP("ModGroup Files"), ":/MO/gui/content/modgroup"}}; + + // Copy the list of enabled contents: + std::vector contents; + std::copy_if(std::begin(GAMEBRYO_CONTENTS), std::end(GAMEBRYO_CONTENTS), + std::back_inserter(contents), [this](auto e) { + return m_Enabled[e.id()]; + }); + return contents; +} + +std::vector GamebryoModDataContent::getContentsFor( + std::shared_ptr fileTree) const +{ + std::vector contents; + + for (auto e : *fileTree) { + if (e->isFile()) { + auto suffix = e->suffix().toLower(); + if (m_Enabled[CONTENT_PLUGIN] && + (suffix == "esp" || suffix == "esm" || suffix == "esl")) { + contents.push_back(CONTENT_PLUGIN); + } else if (m_Enabled[CONTENT_BSA] && (suffix == "bsa" || suffix == "ba2")) { + contents.push_back(CONTENT_BSA); + } else if (m_Enabled[CONTENT_INI] && suffix == "ini" && + e->compare("meta.ini") != 0) { + contents.push_back(CONTENT_INI); + } else if (m_Enabled[CONTENT_MODGROUP] && suffix == "modgroups") { + contents.push_back(CONTENT_MODGROUP); + } + } else { + if (m_Enabled[CONTENT_TEXTURE] && + (e->compare("textures") == 0 || e->compare("icons") == 0 || + e->compare("bookart") == 0)) { + contents.push_back(CONTENT_TEXTURE); + } else if (m_Enabled[CONTENT_MESH] && e->compare("meshes") == 0) { + contents.push_back(CONTENT_MESH); + } else if (m_Enabled[CONTENT_INTERFACE] && + (e->compare("interface") == 0 || e->compare("menus") == 0)) { + contents.push_back(CONTENT_INTERFACE); + } else if (m_Enabled[CONTENT_SOUND] && e->compare("music") == 0 || + e->compare("sound") == 0) { + contents.push_back(CONTENT_SOUND); + } else if (m_Enabled[CONTENT_SCRIPT] && e->compare("scripts") == 0) { + contents.push_back(CONTENT_SCRIPT); + } else if (m_Enabled[CONTENT_SKYPROC] && e->compare("SkyProc Patchers") == 0) { + contents.push_back(CONTENT_SKYPROC); + } else if (m_Enabled[CONTENT_MCM] && e->compare("MCM") == 0) { + contents.push_back(CONTENT_MCM); + } else if (m_Enabled[CONTENT_OPTIONAL] && e->compare("Optional") == 0 && + e->astree()->size() > 0) { + contents.push_back(CONTENT_OPTIONAL); + } + } + } + + if (m_Enabled[CONTENT_FACEGEN]) { + auto e1 = fileTree->findDirectory("meshes/actors/character/facegendata"); + if (e1) { + contents.push_back(CONTENT_FACEGEN); + } else { + auto e2 = fileTree->findDirectory("textures/actors/character/facegendata"); + if (e2) { + contents.push_back(CONTENT_FACEGEN); + } + } + } + + auto extender = m_GameFeatures->gameFeature(); + if (extender != nullptr) { + auto e = fileTree->findDirectory(extender->PluginPath()); + if (e) { + if (m_Enabled[CONTENT_SKSE_FILES]) { + contents.push_back(CONTENT_SKSE_FILES); + } + if (m_Enabled[CONTENT_SKSE]) { + for (auto f : *e) { + if (f->hasSuffix("dll")) { + contents.push_back(CONTENT_SKSE); + break; + } + } + } + } + } + + return contents; +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.h b/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.h new file mode 100644 index 0000000..be02331 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.h @@ -0,0 +1,75 @@ +#ifndef GAMEBRYO_MODDATACONTENT_H +#define GAMEBRYO_MODDATACONTENT_H + +#include +#include + +namespace MOBase +{ +class IGameFeatures; +} + +/** + * @brief ModDataContent for GameBryo games. + * + */ +class GamebryoModDataContent : public MOBase::ModDataContent +{ +protected: + /** + * Note: These are used to index m_Enabled so should have standard + * enum values, not custom ones. + */ + enum EContent + { + CONTENT_PLUGIN, + CONTENT_OPTIONAL, + CONTENT_TEXTURE, + CONTENT_MESH, + CONTENT_BSA, + CONTENT_INTERFACE, + CONTENT_SOUND, + CONTENT_SCRIPT, + CONTENT_SKSE, + CONTENT_SKSE_FILES, + CONTENT_SKYPROC, + CONTENT_MCM, + CONTENT_INI, + CONTENT_FACEGEN, + CONTENT_MODGROUP + }; + + /** + * This is the first value that can be used for game-specific contents. + */ + constexpr static auto CONTENT_NEXT_VALUE = CONTENT_MODGROUP + 1; + +public: + /** + * + */ + GamebryoModDataContent(const MOBase::IGameFeatures* gameFeatures); + + /** + * @return the list of all possible contents for the corresponding game. + */ + virtual std::vector getAllContents() const override; + + /** + * @brief Retrieve the list of contents in the given tree. + * + * @param fileTree The tree corresponding to the mod to retrieve contents for. + * + * @return the IDs of the content in the given tree. + */ + virtual std::vector + getContentsFor(std::shared_ptr fileTree) const override; + +protected: + MOBase::IGameFeatures const* const m_GameFeatures; + + // List of enabled contents: + std::vector m_Enabled; +}; + +#endif // GAMEBRYO_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp b/libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp new file mode 100644 index 0000000..523df0d --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp @@ -0,0 +1,607 @@ +#include "gamebryosavegame.h" + +#include "iplugingame.h" +#include "log.h" +#include "scriptextender.h" + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif +#include +#include + +#include +#include + +#include "gamegamebryo.h" +#include "imoinfo.h" + +#define CHUNK 16384 + +GamebryoSaveGame::GamebryoSaveGame(QString const& file, GameGamebryo const* game, + bool const lightEnabled, bool const mediumEnabled) + : m_FileName(file), m_CreationTime(QFileInfo(file).lastModified()), m_Game(game), + m_MediumEnabled(mediumEnabled), m_LightEnabled(lightEnabled), + m_DataFields([this]() { + return fetchDataFields(); + }) +{} + +GamebryoSaveGame::~GamebryoSaveGame() {} + +QString GamebryoSaveGame::getFilepath() const +{ + return m_FileName; +} + +QDateTime GamebryoSaveGame::getCreationTime() const +{ + return m_CreationTime; +} + +QString GamebryoSaveGame::getName() const +{ + return QObject::tr("%1, #%2, Level %3, %4") + .arg(m_PCName) + .arg(m_SaveNumber) + .arg(m_PCLevel) + .arg(m_PCLocation); +} + +QString GamebryoSaveGame::getSaveGroupIdentifier() const +{ + return m_PCName; +} + +QStringList GamebryoSaveGame::allFiles() const +{ + // This returns all valid files associated with this game + QStringList res = {m_FileName}; + auto e = m_Game->m_Organizer->gameFeatures()->gameFeature(); + if (e != nullptr) { + QFileInfo file(m_FileName); + QFileInfo SEfile(file.absolutePath() + "/" + file.completeBaseName() + "." + + m_Game->savegameSEExtension()); + if (SEfile.exists()) { + res.push_back(SEfile.absoluteFilePath()); + } + } + return res; +} + +bool GamebryoSaveGame::hasScriptExtenderFile() const +{ + QFileInfo file(m_FileName); + QFileInfo SEfile(file.absolutePath() + "/" + file.completeBaseName() + "." + + m_Game->savegameSEExtension()); + return SEfile.exists(); +} + +void GamebryoSaveGame::setCreationTime(_SYSTEMTIME const& ctime) +{ + QDate date; + date.setDate(ctime.wYear, ctime.wMonth, ctime.wDay); + QTime time; + time.setHMS(ctime.wHour, ctime.wMinute, ctime.wSecond, ctime.wMilliseconds); + + m_CreationTime = QDateTime(date, time, Qt::UTC); +} + +GamebryoSaveGame::FileWrapper::FileWrapper(QString const& filepath, + QString const& expected) + : m_File(filepath), m_HasFieldMarkers(false), + m_PluginString(StringType::TYPE_WSTRING), + m_PluginStringFormat(StringFormat::UTF8), m_NextChunk(0) +{ + if (!m_File.open(QIODevice::ReadOnly)) { + throw std::runtime_error( + QObject::tr("failed to open %1").arg(filepath).toUtf8().constData()); + } + + std::vector fileID(expected.length() + 1); + m_File.read(fileID.data(), expected.length()); + fileID[expected.length()] = '\0'; + + QString id(fileID.data()); + if (expected != id) { + throw std::runtime_error( + QObject::tr("wrong file format - expected %1 got \'%2\' for %3") + .arg(expected) + .arg(id) + .arg(filepath) + .toUtf8() + .constData()); + } +} + +void GamebryoSaveGame::FileWrapper::setHasFieldMarkers(bool state) +{ + m_HasFieldMarkers = state; +} + +void GamebryoSaveGame::FileWrapper::setPluginString(StringType type) +{ + m_PluginString = type; +} + +void GamebryoSaveGame::FileWrapper::setPluginStringFormat(StringFormat type) +{ + m_PluginStringFormat = type; +} + +void GamebryoSaveGame::FileWrapper::readQDataStream(QDataStream& data, void* buff, + std::size_t length) +{ + int read = data.readRawData(static_cast(buff), static_cast(length)); + bool result = true; + if (read != length && m_CompressionType == 1) { + bool result = readNextChunk(); + if (result) { + read += data.readRawData(static_cast(buff) + read, + static_cast(length - read)); + } + } + if (read != length || !result) { + throw std::runtime_error("unexpected end of file"); + } +} + +template +void GamebryoSaveGame::FileWrapper::readQDataStream(QDataStream& data, T& value) +{ + static_assert(std::is_trivial_v && std::is_standard_layout_v); + readQDataStream(data, &value, sizeof(T)); +} + +void GamebryoSaveGame::FileWrapper::skipQDataStream(QDataStream& data, + std::size_t length) +{ + int skip = data.skipRawData(static_cast(length)); + bool result = true; + if (skip != length && m_CompressionType == 1) { + result = readNextChunk(); + if (result) { + skip += data.skipRawData(static_cast(length - skip)); + } + } + if (skip != length || !result) { + throw std::runtime_error("unexpected end of file"); + } +} + +template <> +void GamebryoSaveGame::FileWrapper::read(QString& value) +{ + if (m_CompressionType == 0) { + unsigned short length; + if (m_PluginString == StringType::TYPE_BSTRING || + m_PluginString == StringType::TYPE_BZSTRING) { + unsigned char len; + read(len); + length = m_PluginString == StringType::TYPE_BZSTRING ? len + 1 : len; + } else { + read(length); + } + + if (m_HasFieldMarkers) { + skip(); + } + + QByteArray buffer; + buffer.resize(length); + + read(buffer.data(), + m_PluginString == StringType::TYPE_BZSTRING ? length - 1 : length); + + if (m_PluginString == StringType::TYPE_BZSTRING) + buffer[length - 1] = '\0'; + + if (m_HasFieldMarkers) { + skip(); + } + + if (m_PluginStringFormat == StringFormat::UTF8) + value = QString::fromUtf8(buffer.constData()); + else + value = QString::fromLocal8Bit(buffer.constData()); + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + unsigned short length; + if (m_PluginString == StringType::TYPE_BSTRING || + m_PluginString == StringType::TYPE_BZSTRING) { + unsigned char len; + readQDataStream(*m_Data, len); + length = m_PluginString == StringType::TYPE_BZSTRING ? len + 1 : len; + } else { + readQDataStream(*m_Data, length); + } + + if (m_HasFieldMarkers) { + skip(); + } + + QByteArray buffer; + buffer.resize(length); + + readQDataStream(*m_Data, buffer.data(), + m_PluginString == StringType::TYPE_BZSTRING ? length - 1 : length); + + if (m_PluginString == StringType::TYPE_BZSTRING) + buffer[length - 1] = '\0'; + + if (m_HasFieldMarkers) { + m_Data->skipRawData(1); + } + + if (m_PluginStringFormat == StringFormat::UTF8) + value = QString::fromUtf8(buffer.constData()); + else + value = QString::fromLocal8Bit(buffer.constData()); + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + } +} + +void GamebryoSaveGame::FileWrapper::read(void* buff, std::size_t length) +{ + int read = m_File.read(static_cast(buff), length); + if (read != length) { + throw std::runtime_error("unexpected end of file"); + } +} + +QImage GamebryoSaveGame::FileWrapper::readImage(int scale, bool alpha) +{ + uint32_t width; + read(width); + uint32_t height; + read(height); + return readImage(width, height, scale, alpha); +} + +QImage GamebryoSaveGame::FileWrapper::readImage(uint32_t width, uint32_t height, int scale, + bool alpha) +{ + int bpp = alpha ? 4 : 3; + QScopedArrayPointer buffer(new unsigned char[width * height * bpp]); + read(buffer.data(), width * height * bpp); + QImage image(buffer.data(), width, height, + alpha ? QImage::Format_RGBA8888_Premultiplied : QImage::Format_RGB888); + + // We need to copy the image here because QImage does not make a copy of the + // buffer when constructed. + if (scale != 0) { + return image.copy().scaledToWidth(scale); + } else { + return image.copy(); + } +} + +void GamebryoSaveGame::FileWrapper::setCompressionType(uint16_t compressionType) +{ + m_CompressionType = compressionType; +} + +void GamebryoSaveGame::FileWrapper::closeCompressedData() +{ + if (m_CompressionType == 0) { + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + m_NextChunk = 0; + m_UncompressedSize = 0; + m_Data->device()->close(); + delete m_Data; + } else + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); +} + +bool GamebryoSaveGame::FileWrapper::openCompressedData(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + return false; + } else if (m_CompressionType == 1) { + read(m_NextChunk); + read(m_UncompressedSize); + QByteArray placeholder; + m_Data = new QDataStream(placeholder); + bool result = readNextChunk(); + if (result) + skipQDataStream(*m_Data, bytesToIgnore); + return result; + } else if (m_CompressionType == 2) { + uint32_t uncompressedSize; + read(uncompressedSize); + uint32_t compressedSize; + read(compressedSize); + QByteArray compressed; + compressed.resize(compressedSize); + read(compressed.data(), compressedSize); + QByteArray decompressed; + decompressed.resize(uncompressedSize); + LZ4_decompress_safe_partial(compressed.data(), decompressed.data(), compressedSize, + uncompressedSize, uncompressedSize); + compressed.clear(); + + m_Data = new QDataStream(decompressed); + skipQDataStream(*m_Data, bytesToIgnore); + + return true; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return false; + } +} + +bool GamebryoSaveGame::FileWrapper::readNextChunk() +{ + uint32_t have; + uint64_t read = 0; + std::unique_ptr inBuffer = std::make_unique(CHUNK); + std::unique_ptr outBuffer = std::make_unique(CHUNK); + QByteArray finalData; + m_Data->device()->close(); + delete m_Data; + z_stream stream{}; + try { + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = 0; + stream.next_in = Z_NULL; + if (m_NextChunk >= m_File.size() || finalData.size() == m_UncompressedSize) + return false; + m_File.seek(m_NextChunk); + int zlibRet = inflateInit2(&stream, 15 + 32); + if (zlibRet != Z_OK) { + return false; + } + do { + stream.avail_in = m_File.read(inBuffer.get(), CHUNK); + read += stream.avail_in; + if (!m_File.isReadable()) { + (void)inflateEnd(&stream); + return false; + } + if (stream.avail_in == 0) + break; + stream.next_in = reinterpret_cast(inBuffer.get()); + do { + stream.avail_out = CHUNK; + stream.next_out = reinterpret_cast(outBuffer.get()); + zlibRet = inflate(&stream, Z_NO_FLUSH); + if ((zlibRet != Z_OK) && (zlibRet != Z_STREAM_END) && + (zlibRet != Z_BUF_ERROR)) { + return false; + } + have = CHUNK - stream.avail_out; + finalData += QByteArray::fromRawData(outBuffer.get(), have); + } while (stream.avail_out == 0); + read -= stream.avail_in; + } while (zlibRet != Z_STREAM_END); + inflateEnd(&stream); + uint64_t remainder = (m_NextChunk + read) % 16; + uint64_t next = m_NextChunk + read + 16 - (remainder == 0 ? 16 : remainder); + m_NextChunk = next; + } catch (const std::exception&) { + inflateEnd(&stream); + return false; + } + m_Data = new QDataStream(finalData); + return true; +} + +uint8_t GamebryoSaveGame::FileWrapper::readChar(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint8_t version; + read(version); + return version; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint8_t version; + readQDataStream(*m_Data, version); + return version; + + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +uint16_t GamebryoSaveGame::FileWrapper::readShort(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint16_t size; + read(size); + return size; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint16_t size; + readQDataStream(*m_Data, size); + return size; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +uint32_t GamebryoSaveGame::FileWrapper::readInt(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint32_t size; + read(size); + return size; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint32_t size; + readQDataStream(*m_Data, size); + return size; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +uint64_t GamebryoSaveGame::FileWrapper::readLong(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint64_t size; + read(size); + return size; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint64_t size; + readQDataStream(*m_Data, size); + return size; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +float_t GamebryoSaveGame::FileWrapper::readFloat(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + float_t value; + read(value); + return value; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + float_t value; + readQDataStream(*m_Data, value); + return value; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +QStringList GamebryoSaveGame::FileWrapper::readPlugins(int bytesToIgnore, int extraData, + const QStringList& corePlugins) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint8_t count; + read(count); + return readPluginData(count, extraData, corePlugins); + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + skipQDataStream(*m_Data, bytesToIgnore); + uint8_t count; + readQDataStream(*m_Data, count); + return readPluginData(count, extraData, corePlugins); + } + return {}; +} + +QStringList +GamebryoSaveGame::FileWrapper::readLightPlugins(int bytesToIgnore, int extraData, + const QStringList& corePlugins) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint16_t count; + read(count); + return readPluginData(count, extraData, corePlugins); + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + skipQDataStream(*m_Data, bytesToIgnore); + uint16_t count; + readQDataStream(*m_Data, count); + return readPluginData(count, extraData, corePlugins); + } + return {}; +} + +QStringList +GamebryoSaveGame::FileWrapper::readMediumPlugins(int bytesToIgnore, int extraData, + const QStringList& corePlugins) +{ + if (m_CompressionType != 1) { + return {}; + } else { + skipQDataStream(*m_Data, bytesToIgnore); + uint32_t count; + readQDataStream(*m_Data, count); + return readPluginData(count, extraData, corePlugins); + } +} + +QStringList GamebryoSaveGame::FileWrapper::readPluginData(uint32_t count, int extraData, + const QStringList corePlugins) +{ + QStringList plugins; + plugins.reserve(count); + if (m_CompressionType == 0) { + for (std::size_t i = 0; i < count; ++i) { + QString name; + read(name); + plugins.push_back(name); + } + } else { + for (std::size_t i = 0; i < count; ++i) { + QString name; + read(name); + plugins.push_back(name); + bool isCustomPlugin; + if (extraData) { + if (extraData > 1) { + readQDataStream(*m_Data, isCustomPlugin); + } else { + isCustomPlugin = !corePlugins.contains(name); + } + if (isCustomPlugin) { + QString creationName; + QString creationId; + uint16_t flagsSize; + uint8_t isCreation; + read(creationName); + read(creationId); + readQDataStream(*m_Data, flagsSize); + skipQDataStream(*m_Data, flagsSize); + readQDataStream(*m_Data, isCreation); + } + } + } + } + return plugins; +} + +void GamebryoSaveGame::FileWrapper::close() +{ + m_File.close(); +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegame.h b/libs/game_bethesda/src/gamebryo/gamebryosavegame.h new file mode 100644 index 0000000..43dea11 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryosavegame.h @@ -0,0 +1,256 @@ +#ifndef GAMEBRYOSAVEGAME_H +#define GAMEBRYOSAVEGAME_H + +#include "isavegame.h" +#include "memoizedlock.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef _WIN32 +struct _SYSTEMTIME; +#else +// Windows SYSTEMTIME struct - used in Bethesda savegame binary format +struct _SYSTEMTIME { + uint16_t wYear; + uint16_t wMonth; + uint16_t wDayOfWeek; + uint16_t wDay; + uint16_t wHour; + uint16_t wMinute; + uint16_t wSecond; + uint16_t wMilliseconds; +}; +#endif + +namespace MOBase +{ +class IPluginGame; +} // namespace MOBase + +class GameGamebryo; + +class GamebryoSaveGame : public MOBase::ISaveGame +{ +public: + GamebryoSaveGame(QString const& file, GameGamebryo const* game, + bool const lightEnabled = false, bool const mediumEnabled = false); + + virtual ~GamebryoSaveGame(); + +public: // ISaveGame interface + virtual QString getFilepath() const override; + virtual QDateTime getCreationTime() const override; + virtual QString getName() const override; + virtual QString getSaveGroupIdentifier() const override; + virtual QStringList allFiles() const override; + +public: + bool hasScriptExtenderFile() const; + + // Simple getters + virtual QString getPCName() const { return m_PCName; } + virtual unsigned short getPCLevel() const { return m_PCLevel; } + virtual QString getPCLocation() const { return m_PCLocation; } + virtual uint32_t getSaveNumber() const { return m_SaveNumber; } + + QStringList const& getPlugins() const { return m_DataFields.value()->Plugins; } + QStringList const& getMediumPlugins() const + { + return m_DataFields.value()->MediumPlugins; + } + QStringList const& getLightPlugins() const + { + return m_DataFields.value()->LightPlugins; + } + QImage const& getScreenshot() const { return m_DataFields.value()->Screenshot; } + + bool isMediumEnabled() const { return m_MediumEnabled; } + + bool isLightEnabled() const { return m_LightEnabled; } + + enum class StringType + { + TYPE_BZSTRING, + TYPE_BSTRING, + TYPE_WSTRING + }; + + enum class StringFormat + { + UTF8, + LOCAL8BIT + }; + +protected: + friend class FileWrapper; + + class FileWrapper + { + public: + /** + * @brief Construct the save file information. + * + * @param filepath The path to the save file. + * @params expected Expecte bytes at start of file. + * + **/ + FileWrapper(QString const& filepath, QString const& expected); + + /** Set this for save games that have a marker at the end of each + * field. Specifically fallout + **/ + void setHasFieldMarkers(bool); + + /** Set bz string mode (1 byte length, null terminated) + **/ + void setPluginString(StringType); + + /** Set string format (utf-8, windows local 8 bit strings) + **/ + void setPluginStringFormat(StringFormat); + + template + void skip(int count = 1) + { + if (!m_File.seek(m_File.pos() + count * sizeof(T))) { + throw std::runtime_error("unexpected end of file"); + } + } + + template + void read(T& value) + { + int read = m_File.read(reinterpret_cast(&value), sizeof(T)); + if (read != sizeof(T)) { + throw std::runtime_error("unexpected end of file"); + } + if (m_HasFieldMarkers) { + skip(); + } + } + + void seek(unsigned long pos) + { + if (!m_File.seek(pos)) { + throw std::runtime_error("unexpected end of file"); + } + } + + void read(void* buff, std::size_t length); + + /* Reads RGB image from save + * Assumes picture dimentions come immediately before the save + */ + QImage readImage(int scale = 0, bool alpha = false); + + /* Reads RGB image from save */ + QImage readImage(uint32_t width, uint32_t height, int scale = 0, + bool alpha = false); + + /* Sets the compression type. */ + void setCompressionType(uint16_t type); + + /* uncompress the begining of the compressed block */ + bool openCompressedData(int bytesToIgnore = 0); + + /* read the next compressed block */ + bool readNextChunk(); + + /* frees the uncompressed block */ + void closeCompressedData(); + + /* Read the save game version in the compressed block */ + uint8_t readChar(int bytesToIgnore = 0); + + uint16_t readShort(int bytesToIgnore = 0); + + uint32_t readInt(int bytesToIgnore = 0); + + uint64_t readLong(int bytesToIgnore = 0); + + float_t readFloat(int bytesToIgnore = 0); + + /* Read the plugin list */ + QStringList readPlugins(int bytesToIgnore = 0, int extraData = 0, + const QStringList& corePlugins = {}); + + /* Read the light plugin list */ + QStringList readLightPlugins(int bytesToIgnore = 0, int extraData = 0, + const QStringList& corePlugins = {}); + + /* Read the medium plugin list */ + QStringList readMediumPlugins(int bytesToIgnore = 0, int extraData = 0, + const QStringList& corePlugins = {}); + + void close(); + + private: + QFile m_File; + uint64_t m_NextChunk; + uint64_t m_UncompressedSize; + bool m_HasFieldMarkers; + StringType m_PluginString; + StringFormat m_PluginStringFormat; + QDataStream* m_Data; + uint16_t m_CompressionType = 0; + + private: + template + void readQDataStream(QDataStream& data, T& value); + + void readQDataStream(QDataStream& data, void* buff, std::size_t length); + + void skipQDataStream(QDataStream& data, std::size_t length); + + QStringList readPluginData(uint32_t count, int extraData, + const QStringList corePlugins); + }; + + void setCreationTime(_SYSTEMTIME const& time); + + GameGamebryo const* m_Game; + bool m_MediumEnabled; + bool m_LightEnabled; + + QString m_FileName; + QString m_PCName; + unsigned short m_PCLevel; + QString m_PCLocation; + uint32_t m_SaveNumber; + QDateTime m_CreationTime; + + // Those three fields are usually much slower to fetch than + // the other, so we do not fetch them if not needed. + // + // This is virtual so child class can add fields if those are + // hard to access. + struct DataFields + { + QStringList Plugins; + QStringList LightPlugins; + QStringList MediumPlugins; + QImage Screenshot; + + // We need this constructor. + DataFields() {} + virtual ~DataFields() {} + }; + MOBase::MemoizedLocked> m_DataFields; + + // Fetch the field. + virtual std::unique_ptr fetchDataFields() const = 0; +}; + +// Explicit template specialization must be at namespace scope (GCC requirement) +template <> +void GamebryoSaveGame::FileWrapper::read(QString& value); + +#endif // GAMEBRYOSAVEGAME_H diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.cpp b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.cpp new file mode 100644 index 0000000..6d1289a --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.cpp @@ -0,0 +1,100 @@ +#include "gamebryosavegameinfo.h" + +#include "gamebryosavegame.h" +#include "gamebryosavegameinfowidget.h" +#include "gamegamebryo.h" +#include "imodinterface.h" +#include "imoinfo.h" +#include "iplugingame.h" +#include "ipluginlist.h" + +#include +#include +#include + +GamebryoSaveGameInfo::GamebryoSaveGameInfo(GameGamebryo const* game) : m_Game(game) {} + +GamebryoSaveGameInfo::~GamebryoSaveGameInfo() {} + +GamebryoSaveGameInfo::MissingAssets +GamebryoSaveGameInfo::getMissingAssets(MOBase::ISaveGame const& save) const +{ + GamebryoSaveGame const& gamebryoSave = dynamic_cast(save); + MOBase::IOrganizer* organizerCore = m_Game->m_Organizer; + + // collect the list of missing plugins + MissingAssets missingAssets; + + for (QString const& pluginName : gamebryoSave.getPlugins()) { + switch (organizerCore->pluginList()->state(pluginName)) { + case MOBase::IPluginList::STATE_INACTIVE: + missingAssets[pluginName] = + ProvidingModules{organizerCore->pluginList()->origin(pluginName)}; + break; + case MOBase::IPluginList::STATE_MISSING: + missingAssets[pluginName] = ProvidingModules(); + break; + } + } + + for (QString const& pluginName : gamebryoSave.getLightPlugins()) { + switch (organizerCore->pluginList()->state(pluginName)) { + case MOBase::IPluginList::STATE_INACTIVE: + missingAssets[pluginName] = + ProvidingModules{organizerCore->pluginList()->origin(pluginName)}; + break; + case MOBase::IPluginList::STATE_MISSING: + missingAssets[pluginName] = ProvidingModules(); + break; + } + } + + // Find out any other mods that might contain the esp/esm + QStringList espFilter({"*.esp", "*.esl", "*.esm"}); + + QString dataDir(organizerCore->managedGame()->dataDirectory().absolutePath()); + + // Search normal mods. A note: This will also find mods in data. + for (QString const& mod : organizerCore->modList()->allModsByProfilePriority()) { + MOBase::IModInterface* modInfo = organizerCore->modList()->getMod(mod); + QStringList esps = QDir(modInfo->absolutePath()).entryList(espFilter); + for (QString const& esp : esps) { + MissingAssets::iterator iter = missingAssets.find(esp); + if (modInfo->absolutePath() == dataDir) { + // We have to prune esps that reside in the data directory, otherwise + // you get all the unmanaged mods listed as potential candidates for + // enabling + if (modInfo->name() != organizerCore->pluginList()->origin(esp)) { + continue; + } + } + if (iter != missingAssets.end()) { + if (!iter->contains(modInfo->name())) { + iter->push_back(modInfo->name()); + } + } + } + } + + // search in overwrite + { + QDir overwriteDir(organizerCore->overwritePath()); + QStringList esps = overwriteDir.entryList(espFilter); + for (const QString& esp : esps) { + MissingAssets::iterator iter = missingAssets.find(esp); + if (iter != missingAssets.end()) { + if (!iter->contains("")) { + iter->push_back(""); + } + } + } + } + + return missingAssets; +} + +MOBase::ISaveGameInfoWidget* +GamebryoSaveGameInfo::getSaveGameWidget(QWidget* parent) const +{ + return new GamebryoSaveGameInfoWidget(this, parent); +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.h b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.h new file mode 100644 index 0000000..e214293 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.h @@ -0,0 +1,23 @@ +#ifndef GAMEBRYOSAVEGAMEINFO_H +#define GAMEBRYOSAVEGAMEINFO_H + +#include "savegameinfo.h" + +class GameGamebryo; + +class GamebryoSaveGameInfo : public MOBase::SaveGameInfo +{ +public: + GamebryoSaveGameInfo(GameGamebryo const* game); + ~GamebryoSaveGameInfo(); + + virtual MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const override; + + virtual MOBase::ISaveGameInfoWidget* getSaveGameWidget(QWidget*) const override; + +protected: + friend class GamebryoSaveGameInfoWidget; + GameGamebryo const* m_Game; +}; + +#endif // GAMEBRYOSAVEGAMEINFO_H diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.cpp b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.cpp new file mode 100644 index 0000000..8c95dad --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.cpp @@ -0,0 +1,199 @@ +#include "gamebryosavegameinfowidget.h" +#include "ui_gamebryosavegameinfowidget.h" + +#include "gamebryosavegame.h" +#include "gamebryosavegameinfo.h" +#include "gamegamebryo.h" +#include "imoinfo.h" +#include "ipluginlist.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +GamebryoSaveGameInfoWidget::GamebryoSaveGameInfoWidget(GamebryoSaveGameInfo const* info, + QWidget* parent) + : MOBase::ISaveGameInfoWidget(parent), ui(new Ui::GamebryoSaveGameInfoWidget), + m_Info(info) +{ + ui->setupUi(this); + this->setWindowFlags(Qt::ToolTip | Qt::BypassGraphicsProxyWidget); + setWindowOpacity(style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, 0, this) / + qreal(255.0)); + ui->gameFrame->setStyleSheet("background-color: transparent;"); + + QVBoxLayout* gameLayout = new QVBoxLayout(); + gameLayout->setContentsMargins(0, 0, 0, 0); + gameLayout->setSpacing(2); + ui->gameFrame->setLayout(gameLayout); +} + +GamebryoSaveGameInfoWidget::~GamebryoSaveGameInfoWidget() +{ + delete ui; +} + +void GamebryoSaveGameInfoWidget::setSave(MOBase::ISaveGame const& save) +{ + auto& gamebryoSave = dynamic_cast(save); + ui->saveNumLabel->setText(QString("%1").arg(gamebryoSave.getSaveNumber())); + ui->characterLabel->setText(gamebryoSave.getPCName()); + ui->locationLabel->setText(gamebryoSave.getPCLocation()); + ui->levelLabel->setText(QString("%1").arg(gamebryoSave.getPCLevel())); + // This somewhat contorted code is because on my system at least, the + // old way of doing this appears to give short date and long time. + QDateTime t = gamebryoSave.getCreationTime().toLocalTime(); + ui->dateLabel->setText( + QLocale::system().toString(t.date(), QLocale::FormatType::ShortFormat) + " " + + QLocale::system().toString(t.time())); + ui->screenshotLabel->setPixmap(QPixmap::fromImage(gamebryoSave.getScreenshot())); + if (ui->gameFrame->layout() != nullptr) { + QLayoutItem* item = nullptr; + while ((item = ui->gameFrame->layout()->takeAt(0)) != nullptr) { + delete item->widget(); + delete item; + } + ui->gameFrame->layout()->setSizeConstraint(QLayout::SetFixedSize); + } + + // Resize box to new content + this->resize(0, 0); + + QLayout* layout = ui->gameFrame->layout(); + if (gamebryoSave.hasScriptExtenderFile()) { + QLabel* scriptExtender = new QLabel(tr("Has Script Extender Data")); + QFont headerFont = scriptExtender->font(); + headerFont.setBold(true); + layout->addWidget(scriptExtender); + } + QLabel* header = new QLabel(tr("Missing ESPs")); + QFont headerFont = header->font(); + QFont contentFont = headerFont; + headerFont.setItalic(true); + contentFont.setBold(true); + contentFont.setPointSize(7); + header->setFont(headerFont); + layout->addWidget(header); + int count = 0; + MOBase::IPluginList* pluginList = m_Info->m_Game->m_Organizer->pluginList(); + for (QString const& pluginName : gamebryoSave.getPlugins()) { + if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) { + continue; + } + + ++count; + + if (count > 7) { + break; + } + + QLabel* pluginLabel = new QLabel(pluginName); + pluginLabel->setIndent(10); + pluginLabel->setFont(contentFont); + layout->addWidget(pluginLabel); + } + if (count > 7) { + QLabel* dotDotLabel = new QLabel("..."); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (count == 0) { + QLabel* dotDotLabel = new QLabel(tr("None")); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (gamebryoSave.isMediumEnabled()) { + QLabel* headerEsh = new QLabel(tr("Missing ESHs")); + QFont headerEshFont = headerEsh->font(); + QFont contentEshFont = headerEshFont; + headerEshFont.setItalic(true); + contentEshFont.setBold(true); + contentEshFont.setPointSize(7); + headerEsh->setFont(headerEshFont); + layout->addWidget(headerEsh); + int countEsh = 0; + for (QString const& pluginName : gamebryoSave.getMediumPlugins()) { + if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) { + continue; + } + + ++countEsh; + + if (countEsh > 7) { + break; + } + + QLabel* pluginLabel = new QLabel(pluginName); + pluginLabel->setIndent(10); + pluginLabel->setFont(contentFont); + layout->addWidget(pluginLabel); + } + if (countEsh > 7) { + QLabel* dotDotLabel = new QLabel("..."); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (countEsh == 0) { + QLabel* dotDotLabel = new QLabel(tr("None")); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + } + if (gamebryoSave.isLightEnabled()) { + QLabel* headerEsl = new QLabel(tr("Missing ESLs")); + QFont headerEslFont = headerEsl->font(); + QFont contentEslFont = headerEslFont; + headerEslFont.setItalic(true); + contentEslFont.setBold(true); + contentEslFont.setPointSize(7); + headerEsl->setFont(headerEslFont); + layout->addWidget(headerEsl); + int countEsl = 0; + for (QString const& pluginName : gamebryoSave.getLightPlugins()) { + if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) { + continue; + } + + ++countEsl; + + if (countEsl > 7) { + break; + } + + QLabel* pluginLabel = new QLabel(pluginName); + pluginLabel->setIndent(10); + pluginLabel->setFont(contentFont); + layout->addWidget(pluginLabel); + } + if (countEsl > 7) { + QLabel* dotDotLabel = new QLabel("..."); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (countEsl == 0) { + QLabel* dotDotLabel = new QLabel(tr("None")); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + } +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.h b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.h new file mode 100644 index 0000000..6642863 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.h @@ -0,0 +1,30 @@ +#ifndef GAMEBRYOSAVEGAMEINFOWIDGET_H +#define GAMEBRYOSAVEGAMEINFOWIDGET_H + +#include "isavegameinfowidget.h" + +#include + +class GamebryoSaveGameInfo; + +namespace Ui +{ +class GamebryoSaveGameInfoWidget; +} + +class GamebryoSaveGameInfoWidget : public MOBase::ISaveGameInfoWidget +{ + Q_OBJECT + +public: + GamebryoSaveGameInfoWidget(GamebryoSaveGameInfo const* info, QWidget* parent); + ~GamebryoSaveGameInfoWidget(); + + virtual void setSave(MOBase::ISaveGame const&) override; + +private: + Ui::GamebryoSaveGameInfoWidget* ui; + GamebryoSaveGameInfo const* m_Info; +}; + +#endif // GAMEBRYOSAVEGAMEINFOWIDGET_H diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.ui b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.ui new file mode 100644 index 0000000..ea45d10 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.ui @@ -0,0 +1,209 @@ + + + GamebryoSaveGameInfoWidget + + + + 0 + 0 + 400 + 300 + + + + + 0 + 0 + + + + + + + + + + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + + true + + + + Save # + + + + + + + + true + + + + Character + + + + + + + + true + + + + Level + + + + + + + + true + + + + Location + + + + + + + + true + + + + Date + + + + + + + + 75 + true + + + + + + + + + + + + 75 + true + + + + + + + + + + + + 75 + true + + + + + + + + + + + + 75 + true + + + + + + + + + + + + 75 + true + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + false + + + + + + Qt::AlignCenter + + + + + + + + diff --git a/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.cpp b/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.cpp new file mode 100644 index 0000000..e3f709a --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.cpp @@ -0,0 +1,46 @@ +#include "gamebryoscriptextender.h" + +#include "gamegamebryo.h" + +#include "utility.h" + +#include +#include + +GamebryoScriptExtender::GamebryoScriptExtender(const GameGamebryo* game) : m_Game(game) +{} + +GamebryoScriptExtender::~GamebryoScriptExtender() {} + +QString GamebryoScriptExtender::loaderName() const +{ + return BinaryName(); +} + +QString GamebryoScriptExtender::loaderPath() const +{ + return m_Game->gameDirectory().absoluteFilePath(loaderName()); +} + +QString GamebryoScriptExtender::savegameExtension() const +{ + return m_Game->savegameSEExtension(); +} + +bool GamebryoScriptExtender::isInstalled() const +{ + // A note: It is possibly also OK if xxse_steam_loader.dll exists, but it's + // not clear why that would exist and the exe not if you'd installed it per + // instructions, and it'd mess up NCC installs a treat. + return m_Game->gameDirectory().exists(loaderName()); +} + +QString GamebryoScriptExtender::getExtenderVersion() const +{ + return MOBase::getFileVersion(m_Game->gameDirectory().absoluteFilePath(loaderName())); +} + +WORD GamebryoScriptExtender::getArch() const +{ + return m_Game->getArch(loaderName()); +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.h b/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.h new file mode 100644 index 0000000..7ab4f1d --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.h @@ -0,0 +1,31 @@ +#ifndef GAMEBRYOSCRIPTEXTENDER_H +#define GAMEBRYOSCRIPTEXTENDER_H + +#include "scriptextender.h" + +class GameGamebryo; + +class GamebryoScriptExtender : public MOBase::ScriptExtender +{ +public: + GamebryoScriptExtender(GameGamebryo const* game); + + virtual ~GamebryoScriptExtender(); + + virtual QString loaderName() const override; + + virtual QString loaderPath() const override; + + virtual QString savegameExtension() const override; + + virtual bool isInstalled() const override; + + virtual QString getExtenderVersion() const override; + + virtual WORD getArch() const override; + +protected: + GameGamebryo const* const m_Game; +}; + +#endif // GAMEBRYOSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.cpp b/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.cpp new file mode 100644 index 0000000..e5c7608 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.cpp @@ -0,0 +1,51 @@ +#include "gamebryounmanagedmods.h" +#include "gamegamebryo.h" +#include + +GamebryoUnmangedMods::GamebryoUnmangedMods(const GameGamebryo* game) : m_Game(game) {} + +GamebryoUnmangedMods::~GamebryoUnmangedMods() {} + +QStringList GamebryoUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList dlcPlugins = m_Game->DLCPlugins(); + QStringList mainPlugins = m_Game->primaryPlugins(); + + QDir dataDir(m_Game->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!mainPlugins.contains(fileName, Qt::CaseInsensitive) && + (!onlyOfficial || dlcPlugins.contains(fileName, Qt::CaseInsensitive))) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + + return result; +} + +QString GamebryoUnmangedMods::displayName(const QString& modName) const +{ + return modName; +} + +QFileInfo GamebryoUnmangedMods::referenceFile(const QString& modName) const +{ + QFileInfoList files = + m_Game->dataDirectory().entryInfoList(QStringList() << modName + ".es*"); + if (files.size() > 0) { + return files.at(0); + } else { + return QFileInfo(); + } +} + +QStringList GamebryoUnmangedMods::secondaryFiles(const QString& modName) const +{ + QStringList archives; + QDir dataDir = m_Game->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.bsa"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.h b/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.h new file mode 100644 index 0000000..38e2ba5 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.h @@ -0,0 +1,26 @@ +#ifndef GAMEBRYOUNMANAGEDMODS_H +#define GAMEBRYOUNMANAGEDMODS_H + +#include + +class GameGamebryo; + +class GamebryoUnmangedMods : public MOBase::UnmanagedMods +{ +public: + GamebryoUnmangedMods(const GameGamebryo* game); + ~GamebryoUnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; + virtual QString displayName(const QString& modName) const override; + virtual QFileInfo referenceFile(const QString& modName) const override; + virtual QStringList secondaryFiles(const QString& modName) const override; + +protected: + const GameGamebryo* game() const { return m_Game; } + +private: + const GameGamebryo* m_Game; +}; + +#endif // GAMEBRYOUNMANAGEDMODS_H diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp new file mode 100644 index 0000000..ac9fe22 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp @@ -0,0 +1,823 @@ +#include "gamegamebryo.h" + +#include "bsainvalidation.h" +#include "dataarchives.h" +#include "gamebryomoddatacontent.h" +#include "gamebryosavegame.h" +#include "gameplugins.h" +#include "iprofile.h" +#include "log.h" +#include "registry.h" +#include "savegameinfo.h" +#include "scopeguard.h" +#include "scriptextender.h" +#include "utility.h" +#include "vdf_parser.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#include +#endif + +#ifdef HAS_NAK_FFI +#include +#endif + +#include +#include +#include + +// Case-insensitive file path resolution for Linux. +// On Windows (case-insensitive FS), just returns the input path. +// On Linux, if the exact path doesn't exist, searches the parent directory +// for a file matching the name case-insensitively. +static QString resolveFileCaseInsensitive(const QString& filePath) +{ +#ifdef _WIN32 + return filePath; +#else + if (QFileInfo::exists(filePath)) { + return filePath; + } + + QFileInfo info(filePath); + QDir dir(info.path()); + if (!dir.exists()) { + return filePath; + } + + const QString target = info.fileName(); + const QStringList entries = + dir.entryList(QDir::Files | QDir::Hidden | QDir::System); + for (const QString& entry : entries) { + if (entry.compare(target, Qt::CaseInsensitive) == 0) { + return dir.absoluteFilePath(entry); + } + } + + return filePath; +#endif +} + +GameGamebryo::GameGamebryo() {} + +void GameGamebryo::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath(gameName()); +} + +bool GameGamebryo::init(MOBase::IOrganizer* moInfo) +{ + m_Organizer = moInfo; + m_Organizer->onAboutToRun([this](const auto& binary) { + return prepareIni(binary); + }); + return true; +} + +bool GameGamebryo::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +QIcon GameGamebryo::gameIcon() const +{ + return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(binaryName())); +} + +QDir GameGamebryo::gameDirectory() const +{ + return QDir(m_GamePath); +} + +QDir GameGamebryo::dataDirectory() const +{ + return gameDirectory().absoluteFilePath("Data"); +} + +void GameGamebryo::setGamePath(const QString& path) +{ + m_GamePath = path; +} + +QDir GameGamebryo::documentsDirectory() const +{ + return m_MyGamesPath; +} + +QDir GameGamebryo::savesDirectory() const +{ + return QDir(myGamesPath() + "/Saves"); +} + +std::vector> +GameGamebryo::listSaves(QDir folder) const +{ + QStringList filters; + filters << QString("*.") + savegameExtension(); + + std::vector> saves; + for (auto info : folder.entryInfoList(filters, QDir::Files)) { + try { + saves.push_back(makeSaveGame(info.filePath())); + } catch (std::exception& e) { + MOBase::log::error("{}", e.what()); + continue; + } + } + + return saves; +} + +void GameGamebryo::setGameVariant(const QString& variant) +{ + m_GameVariant = variant; +} + +QString GameGamebryo::binaryName() const +{ + return gameShortName() + ".exe"; +} + +MOBase::IPluginGame::LoadOrderMechanism GameGamebryo::loadOrderMechanism() const +{ + return LoadOrderMechanism::FileTime; +} + +MOBase::IPluginGame::SortMechanism GameGamebryo::sortMechanism() const +{ + return SortMechanism::LOOT; +} + +bool GameGamebryo::looksValid(QDir const& path) const +{ + // Check for .exe for now. + return path.exists(binaryName()); +} + +QString GameGamebryo::gameVersion() const +{ + // We try the file version, but if it looks invalid (starts with the fallback + // version), we look the product version instead. If the product version is + // not empty, we use it. + QString binaryAbsPath = gameDirectory().absoluteFilePath(binaryName()); + QString version = MOBase::getFileVersion(binaryAbsPath); + if (version.startsWith(FALLBACK_GAME_VERSION)) { + QString pversion = MOBase::getProductVersion(binaryAbsPath); + if (!pversion.isEmpty()) { + version = pversion; + } + } + return version; +} + +QString GameGamebryo::getLauncherName() const +{ + return gameShortName() + "Launcher.exe"; +} + +WORD GameGamebryo::getArch(QString const& program) const +{ +#ifdef _WIN32 + WORD arch = 0; + // This *really* needs to be factored out + std::wstring app_name = + L"\\\\?\\" + + QDir::toNativeSeparators(this->gameDirectory().absoluteFilePath(program)) + .toStdWString(); + + WIN32_FIND_DATAW FindFileData; + HANDLE hFind = ::FindFirstFileW(app_name.c_str(), &FindFileData); + + // exit if the binary was not found + if (hFind == INVALID_HANDLE_VALUE) + return arch; + + HANDLE hFile = INVALID_HANDLE_VALUE; + HANDLE hMapping = INVALID_HANDLE_VALUE; + LPVOID addrHeader = nullptr; + PIMAGE_NT_HEADERS peHdr = nullptr; + + hFile = CreateFileW(app_name.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + if (hFile == INVALID_HANDLE_VALUE) + goto cleanup; + + hMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, + program.toStdWString().c_str()); + if (hMapping == INVALID_HANDLE_VALUE) + goto cleanup; + + addrHeader = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); + if (addrHeader == NULL) + goto cleanup; // couldn't memory map the file + + peHdr = ImageNtHeader(addrHeader); + if (peHdr == NULL) + goto cleanup; // couldn't read the header + + arch = peHdr->FileHeader.Machine; + +cleanup: // release all of our handles + FindClose(hFind); + if (hFile != INVALID_HANDLE_VALUE) + CloseHandle(hFile); + if (hMapping != INVALID_HANDLE_VALUE) + CloseHandle(hMapping); + return arch; +#else + Q_UNUSED(program); + return 0; +#endif +} + +QFileInfo GameGamebryo::findInGameFolder(const QString& relativePath) const +{ + return QFileInfo(m_GamePath + "/" + relativePath); +} + +QString GameGamebryo::identifyGamePath() const +{ +#ifdef HAS_NAK_FFI + // Use NaK to detect game installations on Linux + NakGameList gameList = nak_detect_all_games(); + ON_BLOCK_EXIT([&]() { + nak_game_list_free(gameList); + }); + + QString shortName = gameShortName(); + QString fullName = gameName(); + + auto tokensMatch = [](const QString& detected, const QString& candidate) { + const QString detectedLower = detected.toLower(); + const QStringList tokens = + candidate.toLower().split(QRegularExpression("[^a-z0-9]+"), + Qt::SkipEmptyParts); + bool anyToken = false; + for (const QString& token : tokens) { + if (token.size() < 3) { + continue; + } + anyToken = true; + if (!detectedLower.contains(token)) { + return false; + } + } + return anyToken; + }; + + for (size_t i = 0; i < gameList.count; ++i) { + const NakGame& game = gameList.games[i]; + QString detectedName = QString::fromUtf8(game.name); + QString detectedPath = QString::fromUtf8(game.install_path); + if (detectedName.compare(fullName, Qt::CaseInsensitive) == 0 || + detectedName.compare(shortName, Qt::CaseInsensitive) == 0 || + detectedName.contains(fullName, Qt::CaseInsensitive) || + detectedName.contains(shortName, Qt::CaseInsensitive) || + tokensMatch(detectedName, fullName) || tokensMatch(detectedName, shortName)) { + if (looksValid(QDir(detectedPath))) { + return detectedPath; + } + } + } + return {}; +#elif defined(_WIN32) + QString path = "Software\\Bethesda Softworks\\" + gameShortName(); + return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + return {}; +#endif +} + +bool GameGamebryo::prepareIni(const QString&) +{ + const auto profile = m_Organizer->profile(); + + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : documentsDirectory().absolutePath(); + + // Ensure all INI files exist with adequate content before writing settings. + // On Linux, the game launcher often doesn't work properly and can't create + // the INI files. If an INI is missing or is a stub (< 200 bytes, meaning + // only MO2-written keys exist), seed it from the game's default INI template. + ensureIniFilesExist(basePath); + + if (!iniFiles().isEmpty()) { + // Resolve case-insensitively (e.g., fallout.ini vs Fallout.ini on Linux) + QString profileIni = + resolveFileCaseInsensitive(basePath + "/" + iniFiles()[0]); + + QString setting = readIniValue(profileIni, "Launcher", "bEnableFileSelection", "0"); + if (setting.toLong() != 1) { + MOBase::WriteRegistryValue("Launcher", "bEnableFileSelection", "1", profileIni); + } + } + + return true; +} + +void GameGamebryo::ensureIniFilesExist(const QString& basePath) +{ + // Make sure the target directory exists + QDir baseDir(basePath); + if (!baseDir.exists()) { + baseDir.mkpath("."); + } + + for (const QString& iniFile : iniFiles()) { + QString targetPath = basePath + "/" + iniFile; + QFileInfo targetInfo(targetPath); + + // Check if the target file already has adequate content + // (> 200 bytes = more than just MO2's BSA invalidation stub) + if (targetInfo.exists() && targetInfo.size() > 200) { + continue; + } + +#ifndef _WIN32 + // On Linux, search for a differently-cased version of this INI file + // (e.g., FalloutPrefs.ini when we expect falloutprefs.ini). + // We can't use resolveFileCaseInsensitive here because the exact-case + // file might exist as an empty stub while the real one has different case. + QString caseMismatchPath; + { + const QString target = targetInfo.fileName(); + const QStringList entries = + baseDir.entryList(QDir::Files | QDir::Hidden | QDir::System); + for (const QString& entry : entries) { + if (entry != target && + entry.compare(target, Qt::CaseInsensitive) == 0) { + caseMismatchPath = baseDir.absoluteFilePath(entry); + break; + } + } + } + + // If a differently-cased version exists with adequate content, + // replace the stub (if any) with a symlink to the real file. + if (!caseMismatchPath.isEmpty()) { + QFileInfo altInfo(caseMismatchPath); + if (altInfo.exists() && altInfo.size() > 200) { + if (targetInfo.exists()) { + QFile::remove(targetPath); + } + QFile::link(altInfo.fileName(), targetPath); + MOBase::log::info("Created case-matching symlink '{}' -> '{}'", + iniFile, altInfo.fileName()); + continue; + } + } +#endif + + // The INI doesn't exist or is a stub, and no adequate differently-cased + // version was found. Try to seed from the game's default INI template + // (e.g., fallout_default.ini, oblivion_default.ini). + QString baseName = QFileInfo(iniFile).completeBaseName(); // "fallout" + QString defaultIniName = baseName + "_default.ini"; + QString defaultIniPath = + resolveFileCaseInsensitive(gameDirectory().absoluteFilePath(defaultIniName)); + + if (QFileInfo::exists(defaultIniPath)) { + // Remove the stub file if it exists so we can replace it + if (targetInfo.exists()) { + QFile::remove(targetPath); + } + if (QFile::copy(defaultIniPath, targetPath)) { + // Make the copy writable + QFile::setPermissions( + targetPath, + QFile::permissions(targetPath) | QFile::WriteUser | QFile::WriteOwner); + MOBase::log::info("Seeded '{}' from default INI '{}'", iniFile, defaultIniPath); + } else { + MOBase::log::warn("Failed to copy default INI '{}' -> '{}'", + defaultIniPath, targetPath); + } + } + } +} + +QString GameGamebryo::readIniValue(const QString& iniFile, const QString& section, + const QString& key, const QString& defaultValue) +{ + // Read INI values directly without QSettings to avoid QSettings + // misinterpreting backslashes as line continuations. + QFile file(iniFile); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return defaultValue; + } + + QString sectionHeader = "[" + section + "]"; + QTextStream in(&file); + bool inSection = false; + + while (!in.atEnd()) { + QString line = in.readLine().trimmed(); + + if (line.startsWith('[') && line.endsWith(']')) { + if (inSection) { + break; // Left the target section without finding key + } + if (line.compare(sectionHeader, Qt::CaseInsensitive) == 0) { + inSection = true; + } + continue; + } + + if (inSection && !line.isEmpty() && !line.startsWith(';') && !line.startsWith('#')) { + int eqPos = line.indexOf('='); + if (eqPos > 0) { + QString existingKey = line.left(eqPos).trimmed(); + if (existingKey.compare(key, Qt::CaseInsensitive) == 0) { + return line.mid(eqPos + 1).trimmed(); + } + } + } + } + + return defaultValue; +} + +QString GameGamebryo::selectedVariant() const +{ + return m_GameVariant; +} + +QString GameGamebryo::myGamesPath() const +{ + return m_MyGamesPath; +} + +/*static*/ QString GameGamebryo::getLootPath() +{ +#ifdef _WIN32 + return findInRegistry(HKEY_LOCAL_MACHINE, L"Software\\LOOT", L"Installed Path") + + "/Loot.exe"; +#else + // On Linux, look for loot in common locations + QString flatpakPath = QDir::homePath() + "/.var/app/io.github.loot.loot/data/LOOT/Loot"; + if (QFileInfo(flatpakPath).exists()) + return flatpakPath; + // Try system PATH + QString systemLoot = QStandardPaths::findExecutable("loot"); + if (!systemLoot.isEmpty()) + return systemLoot; + return "loot"; +#endif +} + +QString GameGamebryo::localAppFolder() +{ +#ifdef _WIN32 + QString result = getKnownFolderPath(FOLDERID_LocalAppData, false); + if (result.isEmpty()) { + // fallback: try the registry + result = getSpecialPath("Local AppData"); + } + return result; +#else + return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); +#endif +} + +void GameGamebryo::copyToProfile(QString const& sourcePath, + QDir const& destinationDirectory, + QString const& sourceFileName) +{ + copyToProfile(sourcePath, destinationDirectory, sourceFileName, sourceFileName); +} + +void GameGamebryo::copyToProfile(QString const& sourcePath, + QDir const& destinationDirectory, + QString const& sourceFileName, + QString const& destinationFileName) +{ + QString filePath = destinationDirectory.absoluteFilePath(destinationFileName); + if (!QFileInfo(filePath).exists()) { + QString sourceFile = sourcePath + "/" + sourceFileName; + // On Linux, try case-insensitive match if the exact source doesn't exist + sourceFile = resolveFileCaseInsensitive(sourceFile); + if (!MOBase::shellCopy(sourceFile, filePath)) { + // if copy file fails, create the file empty + QFile(filePath).open(QIODevice::WriteOnly); + } + } +} + +MappingType GameGamebryo::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameShortName() + "/" + profileFile, + false}); + } + + return result; +} + +#ifdef _WIN32 +std::unique_ptr GameGamebryo::getRegValue(HKEY key, LPCWSTR path, LPCWSTR value, + DWORD flags, LPDWORD type) +{ + DWORD size = 0; + HKEY subKey; + LONG res = ::RegOpenKeyExW(key, path, 0, KEY_QUERY_VALUE | KEY_WOW64_32KEY, &subKey); + if (res != ERROR_SUCCESS) { + res = ::RegOpenKeyExW(key, path, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &subKey); + if (res != ERROR_SUCCESS) + return std::unique_ptr(); + } + res = ::RegGetValueW(subKey, L"", value, flags, type, nullptr, &size); + if (res == ERROR_FILE_NOT_FOUND || res == ERROR_UNSUPPORTED_TYPE) { + return std::unique_ptr(); + } + if (res != ERROR_SUCCESS && res != ERROR_MORE_DATA) { + throw MOBase::MyException( + QObject::tr("failed to query registry path (preflight): %1").arg(res, 0, 16)); + } + + std::unique_ptr result(new BYTE[size]); + res = ::RegGetValueW(subKey, L"", value, flags, type, result.get(), &size); + + if (res != ERROR_SUCCESS) { + throw MOBase::MyException( + QObject::tr("failed to query registry path (read): %1").arg(res, 0, 16)); + } + + return result; +} + +QString GameGamebryo::findInRegistry(HKEY baseKey, LPCWSTR path, LPCWSTR value) +{ + std::unique_ptr buffer = + getRegValue(baseKey, path, value, RRF_RT_REG_SZ | RRF_NOEXPAND); + + return QString::fromUtf16(reinterpret_cast(buffer.get())); +} + +QString GameGamebryo::getKnownFolderPath(REFKNOWNFOLDERID folderId, bool useDefault) +{ + PWSTR path = nullptr; + ON_BLOCK_EXIT([&]() { + if (path != nullptr) + ::CoTaskMemFree(path); + }); + + if (::SHGetKnownFolderPath(folderId, useDefault ? KF_FLAG_DEFAULT_PATH : 0, NULL, + &path) == S_OK) { + return QDir::fromNativeSeparators(QString::fromWCharArray(path)); + } else { + return QString(); + } +} + +QString GameGamebryo::getSpecialPath(const QString& name) +{ + QString base = findInRegistry( + HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders", + name.toStdWString().c_str()); + + WCHAR temp[MAX_PATH]; + if (::ExpandEnvironmentStringsW(base.toStdWString().c_str(), temp, MAX_PATH) != 0) { + return QString::fromWCharArray(temp); + } else { + return base; + } +} +#endif // _WIN32 + +QString GameGamebryo::determineMyGamesPath(const QString& gameName) +{ + const QString pattern = "%1/My Games/" + gameName; + + auto tryDir = [&](const QString& dir) -> std::optional { + if (dir.isEmpty()) { + return {}; + } + + const auto path = pattern.arg(dir); + if (!QFileInfo(path).exists()) { + return {}; + } + + return path; + }; + +#ifdef _WIN32 + // a) this is the way it should work. get the configured My Documents directory + if (auto d = tryDir(getKnownFolderPath(FOLDERID_Documents, false))) { + return *d; + } + + // b) if there is no directory there, look in the default directory + if (auto d = tryDir(getKnownFolderPath(FOLDERID_Documents, true))) { + return *d; + } + + // c) finally, look in the registry. This is discouraged + if (auto d = tryDir(getSpecialPath("Personal"))) { + return *d; + } +#else + // On Linux, My Games is inside the Wine prefix's Documents folder. + // Check common Wine prefix locations for steamuser Documents. + QStringList prefixDocPaths; + + // First check the configured prefix from MO2 settings (most reliable). + const QString configuredPrefix = + QSettings().value("fluorine/prefix_path").toString().trimmed(); + if (!configuredPrefix.isEmpty()) { + const QString configuredDocs = + QDir(configuredPrefix).filePath("drive_c/users/steamuser/Documents"); + prefixDocPaths.append(configuredDocs); + } + + // Standard Steam Proton prefix paths + QString steamRoot = QDir::homePath() + "/.steam/steam/steamapps/compatdata"; + QDir compatDir(steamRoot); + if (compatDir.exists()) { + QStringList appIds = compatDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString& appId : appIds) { + prefixDocPaths.append(steamRoot + "/" + appId + + "/pfx/drive_c/users/steamuser/Documents"); + } + } + + // Also check XDG Documents (for native games or manual setups) + prefixDocPaths.append( + QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); + + for (const QString& docPath : prefixDocPaths) { + if (auto d = tryDir(docPath)) { + MOBase::log::debug("determineMyGamesPath: found '{}' for game '{}'", *d, gameName); + return *d; + } + } + + // If no existing directory was found, try to create it in the configured prefix + // so that the game launcher can populate it on first run. + if (!configuredPrefix.isEmpty()) { + const QString configuredDocs = + QDir(configuredPrefix).filePath("drive_c/users/steamuser/Documents"); + const QString newPath = pattern.arg(configuredDocs); + if (QDir().mkpath(newPath)) { + MOBase::log::info("determineMyGamesPath: created '{}' for game '{}'", newPath, + gameName); + return newPath; + } + } + + MOBase::log::warn("determineMyGamesPath: could not find My Games path for '{}'", + gameName); +#endif + + return {}; +} + +QString GameGamebryo::parseEpicGamesLocation(const QStringList& manifests) +{ +#ifdef _WIN32 + // Use the registry entry to find the EGL Data dir first, just in case something + // changes + QString manifestDir = findInRegistry( + HKEY_LOCAL_MACHINE, L"Software\\Epic Games\\EpicGamesLauncher", L"AppDataPath"); + if (manifestDir.isEmpty()) + manifestDir = getKnownFolderPath(FOLDERID_ProgramData, false) + + "\\Epic\\EpicGamesLauncher\\Data\\"; + manifestDir += "Manifests"; +#else + // Epic Games on Linux (via Heroic or Legendary) + QString manifestDir = QDir::homePath() + + "/.config/heroic/store_cache/egl_manifests"; + if (!QDir(manifestDir).exists()) { + // Try Legendary + manifestDir = QDir::homePath() + "/.config/legendary/installed.json"; + // Legendary uses a different format - skip for now + return ""; + } +#endif + + QDir epicManifests(manifestDir, "*.item", + QDir::SortFlags(QDir::Name | QDir::IgnoreCase), QDir::Files); + if (epicManifests.exists()) { + QDirIterator it(epicManifests); + while (it.hasNext()) { + QString manifestFile = it.next(); + QFile manifest(manifestFile); + + if (!manifest.open(QIODevice::ReadOnly)) { + qWarning("Couldn't open Epic Games manifest file."); + continue; + } + + QByteArray manifestData = manifest.readAll(); + + QJsonDocument manifestJson(QJsonDocument::fromJson(manifestData)); + + if (manifests.contains(manifestJson["AppName"].toString())) { + return manifestJson["InstallLocation"].toString(); + } + } + } + return ""; +} + +QString GameGamebryo::parseSteamLocation(const QString& appid, + const QString& directoryName) +{ +#ifdef HAS_NAK_FFI + // Use NaK to find Steam path + char* steamPathC = nak_find_steam_path(); + QString steamLocation; + if (steamPathC) { + steamLocation = QString::fromUtf8(steamPathC); + nak_string_free(steamPathC); + } +#elif defined(_WIN32) + QString path = "Software\\Valve\\Steam"; + QString steamLocation = + findInRegistry(HKEY_CURRENT_USER, path.toStdWString().c_str(), L"SteamPath"); +#else + // Fallback: common Steam locations on Linux + QString steamLocation = QDir::homePath() + "/.steam/steam"; + if (!QDir(steamLocation).exists()) { + steamLocation = QDir::homePath() + "/.local/share/Steam"; + } +#endif + + if (!steamLocation.isEmpty()) { + QString steamLibraryLocation; + QString steamLibraries(steamLocation + "/" + "config" + "/" + + "libraryfolders.vdf"); + if (QFile(steamLibraries).exists()) { + std::ifstream file(steamLibraries.toStdString()); + auto root = tyti::vdf::read(file); + for (auto child : root.childs) { + tyti::vdf::object* library = child.second.get(); + auto apps = library->childs["apps"]; + if (apps->attribs.contains(appid.toStdString())) { + steamLibraryLocation = QString::fromStdString(library->attribs["path"]); + break; + } + } + } + if (!steamLibraryLocation.isEmpty()) { + if (!directoryName.isEmpty()) { + QString gameLocation = steamLibraryLocation + "/" + "steamapps" + "/" + + "common" + "/" + directoryName; + if (QDir(gameLocation).exists()) { + return gameLocation; + } + } + + // Fallback: resolve install dir from appmanifest_.acf. + QString manifestPath = steamLibraryLocation + "/steamapps/appmanifest_" + appid + + ".acf"; + if (QFile::exists(manifestPath)) { + std::ifstream manifestFile(manifestPath.toStdString()); + auto manifest = tyti::vdf::read(manifestFile); + auto appStateIt = manifest.childs.find("AppState"); + if (appStateIt != manifest.childs.end()) { + const auto& attrs = appStateIt->second->attribs; + auto installdirIt = attrs.find("installdir"); + if (installdirIt != attrs.end()) { + QString installdir = QString::fromStdString(installdirIt->second); + QString gameLocation = steamLibraryLocation + "/steamapps/common/" + installdir; + if (QDir(gameLocation).exists()) { + return gameLocation; + } + } + } + } + } + } + return ""; +} + +void GameGamebryo::registerFeature(std::shared_ptr feature) +{ + // priority does not matter, this is a game plugin so will get lowest priority in MO2 + m_Organizer->gameFeatures()->registerFeature(this, feature, 0, true); +} diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.h b/libs/game_bethesda/src/gamebryo/gamegamebryo.h new file mode 100644 index 0000000..fa0a13d --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.h @@ -0,0 +1,169 @@ +#ifndef GAMEGAMEBRYO_H +#define GAMEGAMEBRYO_H + +#include "iplugingame.h" + +class BSAInvalidation; +class DataArchives; +class LocalSavegames; +class SaveGameInfo; +class BSAInvalidation; +class LocalSavegames; +class ScriptExtender; +class GamePlugins; +class UnmanagedMods; + +#include +#include +#ifdef _WIN32 +#include +#include +#endif +#include +#include +#include + +#include "gamebryosavegame.h" +#include "igamefeatures.h" + +class GameGamebryo : public MOBase::IPluginGame, public MOBase::IPluginFileMapper +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame MOBase::IPluginFileMapper) + + friend class GamebryoScriptExtender; + friend class GamebryoSaveGameInfo; + friend class GamebryoSaveGameInfoWidget; + friend class GamebryoSaveGame; + + /** + * Some Bethesda games do not have a valid file version but a valid product + * version. If the file version starts with FALLBACK_GAME_VERSION, the product + * version will be tried. + */ + static constexpr const char* FALLBACK_GAME_VERSION = "1.0.0"; + +public: + GameGamebryo(); + + void detectGame() override; + bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + // getName + // initializeProfile + virtual std::vector> + listSaves(QDir folder) const override; + + virtual bool isInstalled() const override; + virtual QIcon gameIcon() const override; + virtual QDir gameDirectory() const override; + virtual QDir dataDirectory() const override; + // secondaryDataDirectories + virtual void setGamePath(const QString& path) override; + virtual QDir documentsDirectory() const override; + virtual QDir savesDirectory() const override; + // executables + // steamAPPId + // primaryPlugins + // enabledPlugins + // gameVariants + virtual void setGameVariant(const QString& variant) override; + virtual QString binaryName() const override; + // gameShortName + // primarySources + // validShortNames + // iniFiles + // DLCPlugins + // CCPlugins + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual SortMechanism sortMechanism() const override; + // nexusModOrganizerID + // nexusGameID + virtual bool looksValid(QDir const&) const override; + virtual QString gameVersion() const override; + virtual QString getLauncherName() const override; + +public: // IPluginFileMapper interface + virtual MappingType mappings() const; + +public: // Other (e.g. for game features) + QString myGamesPath() const; + +protected: + // Retrieve the saves extension for the game. + virtual QString savegameExtension() const = 0; + virtual QString savegameSEExtension() const = 0; + + // Create a save game. + virtual std::shared_ptr + makeSaveGame(QString filepath) const = 0; + + QFileInfo findInGameFolder(const QString& relativePath) const; + QString selectedVariant() const; + + static QString localAppFolder(); + // Arguably this shouldn't really be here but every gamebryo program seems to + // use it + static QString getLootPath(); + + // This function is not terribly well named as it copies exactly where it's told + // to, irrespective of whether it's in the profile... + static void copyToProfile(const QString& sourcePath, const QDir& destinationDirectory, + const QString& sourceFileName); + + static void copyToProfile(const QString& sourcePath, const QDir& destinationDirectory, + const QString& sourceFileName, + const QString& destinationFileName); + + virtual QString identifyGamePath() const; + + virtual bool prepareIni(const QString& exec); + +public: + // Ensure all game INI files exist with adequate content in the given + // base path. On Linux, seeds missing/stub INIs from the game's default + // INI templates and creates case-matching symlinks if needed. + void ensureIniFilesExist(const QString& basePath); + +protected: + +#ifdef _WIN32 + static std::unique_ptr getRegValue(HKEY key, LPCWSTR path, LPCWSTR value, + DWORD flags, LPDWORD type); + + static QString findInRegistry(HKEY baseKey, LPCWSTR path, LPCWSTR value); + + static QString getKnownFolderPath(REFKNOWNFOLDERID folderId, bool useDefault); + + static QString getSpecialPath(const QString& name); +#endif + + WORD getArch(QString const& program) const; + + static QString determineMyGamesPath(const QString& gameName); + + static QString parseEpicGamesLocation(const QStringList& manifests); + + static QString parseSteamLocation(const QString& appid, const QString& directoryName); + +public: // Cross-platform INI file utilities (used by game features) + // Read a value from a Bethesda-style INI file using QSettings + static QString readIniValue(const QString& iniFile, const QString& section, + const QString& key, const QString& defaultValue = QString()); + +protected: + void registerFeature(std::shared_ptr feature); + +protected: + // to access organizer for game features, avoid having to pass it to all saves since + // we already pass the game + friend class GamebryoSaveGame; + + QString m_GamePath; + QString m_MyGamesPath; + QString m_GameVariant; + MOBase::IOrganizer* m_Organizer; +}; + +#endif // GAMEGAMEBRYO_H diff --git a/libs/game_bethesda/src/gamebryo/vdf_parser.h b/libs/game_bethesda/src/gamebryo/vdf_parser.h new file mode 100644 index 0000000..53994bf --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/vdf_parser.h @@ -0,0 +1,728 @@ +// MIT License +// +// Copyright(c) 2016 Matthias Moeller +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef __TYTI_STEAM_VDF_PARSER_H__ +#define __TYTI_STEAM_VDF_PARSER_H__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// for wstring support +#include +#include + +// internal +#include + +#include + +// VS < 2015 has only partial C++11 support +#if defined(_MSC_VER) && _MSC_VER < 1900 +#ifndef CONSTEXPR +#define CONSTEXPR +#endif + +#ifndef NOEXCEPT +#define NOEXCEPT +#endif +#else +#ifndef CONSTEXPR +#define CONSTEXPR constexpr +#define TYTI_UNDEF_CONSTEXPR +#endif + +#ifndef NOEXCEPT +#define NOEXCEPT noexcept +#define TYTI_UNDEF_NOEXCEPT +#endif + +#endif + +namespace tyti +{ +namespace vdf +{ + namespace detail + { + /////////////////////////////////////////////////////////////////////////// + // Helper functions selecting the right encoding (char/wchar_T) + /////////////////////////////////////////////////////////////////////////// + + template + struct literal_macro_help + { + static CONSTEXPR const char* result(const char* c, const wchar_t*) NOEXCEPT + { + return c; + } + static CONSTEXPR const char result(const char c, const wchar_t) NOEXCEPT + { + return c; + } + }; + + template <> + struct literal_macro_help + { + static CONSTEXPR const wchar_t* result(const char*, const wchar_t* wc) NOEXCEPT + { + return wc; + } + static CONSTEXPR const wchar_t result(const char, const wchar_t wc) NOEXCEPT + { + return wc; + } + }; +#define TYTI_L(type, text) vdf::detail::literal_macro_help::result(text, L##text) + + inline std::string string_converter(const std::string& w) NOEXCEPT + { + return w; + } + + inline std::string string_converter(const std::wstring& w) + { + return QString::fromStdWString(w).toStdString(); + } + + /////////////////////////////////////////////////////////////////////////// + // Writer helper functions + /////////////////////////////////////////////////////////////////////////// + + template + class tabs + { + const size_t t; + + public: + explicit CONSTEXPR tabs(size_t i) NOEXCEPT : t(i) {} + std::basic_string print() const + { + return std::basic_string(t, TYTI_L(charT, '\t')); + } + inline CONSTEXPR tabs operator+(size_t i) const NOEXCEPT { return tabs(t + i); } + }; + + template + oStreamT& operator<<(oStreamT& s, const tabs t) + { + s << t.print(); + return s; + } + } // end namespace detail + + /////////////////////////////////////////////////////////////////////////// + // Interface + /////////////////////////////////////////////////////////////////////////// + + /// custom objects and their corresponding write functions + + /// basic object node. Every object has a name and can contains attributes saved as + /// key_value pairs or childrens + template + struct basic_object + { + typedef CharT char_type; + std::basic_string name; + std::unordered_map, std::basic_string> + attribs; + std::unordered_map, + std::shared_ptr>> + childs; + + void add_attribute(std::basic_string key, + std::basic_string value) + { + attribs.emplace(std::move(key), std::move(value)); + } + void add_child(std::unique_ptr> child) + { + std::shared_ptr> obj{child.release()}; + childs.emplace(obj->name, obj); + } + void set_name(std::basic_string n) { name = std::move(n); } + }; + + template + struct basic_multikey_object + { + typedef CharT char_type; + std::basic_string name; + std::unordered_multimap, std::basic_string> + attribs; + std::unordered_multimap, + std::shared_ptr>> + childs; + + void add_attribute(std::basic_string key, + std::basic_string value) + { + attribs.emplace(std::move(key), std::move(value)); + } + void add_child(std::unique_ptr> child) + { + std::shared_ptr> obj{child.release()}; + childs.emplace(obj->name, obj); + } + void set_name(std::basic_string n) { name = std::move(n); } + }; + + typedef basic_object object; + typedef basic_object wobject; + typedef basic_multikey_object multikey_object; + typedef basic_multikey_object wmultikey_object; + + struct Options + { + bool strip_escape_symbols; + bool ignore_all_platform_conditionals; + bool ignore_includes; + + Options() + : strip_escape_symbols(true), ignore_all_platform_conditionals(false), + ignore_includes(false) + {} + }; + + // forward decls + // forward decl + template + OutputT read(iStreamT& inStream, const Options& opt = Options{}); + + /** \brief writes given object tree in vdf format to given stream. + Output is prettyfied, using tabs + */ + template + void write(oStreamT& s, const T& r, + const detail::tabs tab = + detail::tabs(0)) + { + typedef typename oStreamT::char_type charT; + using namespace detail; + s << tab << TYTI_L(charT, '"') << r.name << TYTI_L(charT, "\"\n") << tab + << TYTI_L(charT, "{\n"); + for (const auto& i : r.attribs) + s << tab + 1 << TYTI_L(charT, '"') << i.first << TYTI_L(charT, "\"\t\t\"") + << i.second << TYTI_L(charT, "\"\n"); + for (const auto& i : r.childs) + if (i.second) + write(s, *i.second, tab + 1); + s << tab << TYTI_L(charT, "}\n"); + } + + namespace detail + { + template + std::basic_string read_file(iStreamT& inStream) + { + // cache the file + typedef typename iStreamT::char_type charT; + std::basic_string str; + inStream.seekg(0, std::ios::end); + str.resize(static_cast(inStream.tellg())); + if (str.empty()) + return str; + + inStream.seekg(0, std::ios::beg); + inStream.read(&str[0], str.size()); + return str; + } + + /** \brief Read VDF formatted sequences defined by the range [first, last). + If the file is mailformatted, parser will try to read it until it can. + @param first begin iterator + @param end end iterator + @param exclude_files list of files which cant be included anymore. + prevents circular includes + + can thow: + - "std::runtime_error" if a parsing error occured + - "std::bad_alloc" if not enough memory coup be allocated + */ + template + std::vector> + read_internal(IterT first, const IterT last, + std::unordered_set::value_type>>& exclude_files, + const Options& opt) + { + static_assert(std::is_default_constructible::value, + "Output Type must be default constructible (provide constructor " + "without arguments)"); + static_assert(std::is_move_constructible::value, + "Output Type must be move constructible"); + + typedef typename std::iterator_traits::value_type charT; + + const std::basic_string comment_end_str = TYTI_L(charT, "*/"); + const std::basic_string whitespaces = TYTI_L(charT, " \n\v\f\r\t"); + +#ifdef WIN32 + std::function&)> is_platform_str = + [](const std::basic_string& in) { + return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$WINDOWS"); + }; +#elif __APPLE__ + // WIN32 stands for pc in general + std::function&)> is_platform_str = + [](const std::basic_string& in) { + return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$POSIX") || + in == TYTI_L(charT, "$OSX"); + }; + +#elif __linux__ + // WIN32 stands for pc in general + std::function&)> is_platform_str = + [](const std::basic_string& in) { + return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$POSIX") || + in == TYTI_L(charT, "$LINUX"); + }; +#else + std::function&)> is_platform_str = + [](const std::basic_string& in) { + return false; + }; +#endif + + if (opt.ignore_all_platform_conditionals) + is_platform_str = [](const std::basic_string&) { + return false; + }; + + // function for skipping a comment block + // iter: iterator poition to the position after a '/' + auto skip_comments = [&comment_end_str](IterT iter, const IterT& last) -> IterT { + ++iter; + if (iter != last) { + if (*iter == TYTI_L(charT, '/')) { + // line comment, skip whole line + iter = std::find(iter + 1, last, TYTI_L(charT, '\n')); + } + + if (*iter == '*') { + // block comment, skip until next occurance of "*\" + iter = std::search(iter + 1, last, std::begin(comment_end_str), + std::end(comment_end_str)); + iter += 2; + } + } + return iter; + }; + + auto end_quote = [](IterT iter, const IterT& last) -> IterT { + const auto begin = iter; + auto last_esc = iter; + do { + ++iter; + iter = std::find(iter, last, TYTI_L(charT, '\"')); + if (iter == last) + break; + + last_esc = std::prev(iter); + while (last_esc != begin && *last_esc == '\\') + --last_esc; + } while (!(std::distance(last_esc, iter) % 2)); + if (iter == last) + throw std::runtime_error{"quote was opened but not closed."}; + return iter; + }; + + auto end_word = [&whitespaces](IterT iter, const IterT& last) -> IterT { + const auto begin = iter; + auto last_esc = iter; + do { + ++iter; + iter = std::find_first_of(iter, last, std::begin(whitespaces), + std::end(whitespaces)); + if (iter == last) + break; + + last_esc = std::prev(iter); + while (last_esc != begin && *last_esc == '\\') + --last_esc; + } while (!(std::distance(last_esc, iter) % 2)); + // if (iter == last) + // throw std::runtime_error{ "word wasnt properly ended" }; + return iter; + }; + + auto skip_whitespaces = [&whitespaces](IterT iter, const IterT& last) -> IterT { + iter = std::find_if_not(iter, last, [&whitespaces](charT c) { + // return true if whitespace + return std::any_of(std::begin(whitespaces), std::end(whitespaces), + [c](charT pc) { + return pc == c; + }); + }); + return iter; + }; + + std::function&)> strip_escape_symbols = + [](std::basic_string& s) { + auto quote_searcher = [&s](size_t pos) { + return s.find(TYTI_L(charT, "\\\""), pos); + }; + auto p = quote_searcher(0); + while (p != s.npos) { + s.replace(p, 2, TYTI_L(charT, "\"")); + p = quote_searcher(p); + } + auto searcher = [&s](size_t pos) { + return s.find(TYTI_L(charT, "\\\\"), pos); + }; + p = searcher(0); + while (p != s.npos) { + s.replace(p, 2, TYTI_L(charT, "\\")); + p = searcher(p); + } + }; + + if (!opt.strip_escape_symbols) + strip_escape_symbols = [](std::basic_string&) {}; + + auto conditional_fullfilled = [&skip_whitespaces, + &is_platform_str](IterT& iter, const IterT& last) { + iter = skip_whitespaces(iter, last); + if (*iter == '[') { + ++iter; + const auto end = std::find(iter, last, ']'); + const bool negate = *iter == '!'; + if (negate) + ++iter; + auto conditional = std::basic_string(iter, end); + + const bool is_platform = is_platform_str(conditional); + iter = end + 1; + + return static_cast(is_platform ^ negate); + } + return true; + }; + + // read header + // first, quoted name + std::unique_ptr curObj = nullptr; + std::vector> roots; + std::stack> lvls; + auto curIter = first; + + while (curIter != last && *curIter != '\0') { + // find first starting attrib/child, or ending + curIter = skip_whitespaces(curIter, last); + if (curIter == last || *curIter == '\0') + break; + if (*curIter == TYTI_L(charT, '/')) { + curIter = skip_comments(curIter, last); + } else if (*curIter != TYTI_L(charT, '}')) { + + // get key + const auto keyEnd = (*curIter == TYTI_L(charT, '\"')) + ? end_quote(curIter, last) + : end_word(curIter, last); + if (*curIter == TYTI_L(charT, '\"')) + ++curIter; + std::basic_string key(curIter, keyEnd); + strip_escape_symbols(key); + curIter = keyEnd + ((*keyEnd == TYTI_L(charT, '\"')) ? 1 : 0); + + curIter = skip_whitespaces(curIter, last); + + auto conditional = conditional_fullfilled(curIter, last); + if (!conditional) + continue; + + while (*curIter == TYTI_L(charT, '/')) { + + curIter = skip_comments(curIter, last); + if (curIter == last || *curIter == '}') + throw std::runtime_error{"key declared, but no value"}; + curIter = skip_whitespaces(curIter, last); + if (curIter == last || *curIter == '}') + throw std::runtime_error{"key declared, but no value"}; + } + // get value + if (*curIter != '{') { + const auto valueEnd = (*curIter == TYTI_L(charT, '\"')) + ? end_quote(curIter, last) + : end_word(curIter, last); + if (*curIter == TYTI_L(charT, '\"')) + ++curIter; + + auto value = std::basic_string(curIter, valueEnd); + strip_escape_symbols(value); + curIter = valueEnd + ((*valueEnd == TYTI_L(charT, '\"')) ? 1 : 0); + + auto conditional = conditional_fullfilled(curIter, last); + if (!conditional) + continue; + + // process value + if (key != TYTI_L(charT, "#include") && key != TYTI_L(charT, "#base")) { + if (curObj) { + curObj->add_attribute(std::move(key), std::move(value)); + } else { + throw std::runtime_error{"unexpected key without object"}; + } + } else { + if (!opt.ignore_includes && + exclude_files.find(value) == exclude_files.end()) { + exclude_files.insert(value); + std::basic_ifstream i(detail::string_converter(value)); + auto str = read_file(i); + auto file_objs = + read_internal(str.begin(), str.end(), exclude_files, opt); + for (auto& n : file_objs) { + if (curObj) + curObj->add_child(std::move(n)); + else + roots.push_back(std::move(n)); + } + exclude_files.erase(value); + } + } + } else if (*curIter == '{') { + if (curObj) + lvls.push(std::move(curObj)); + curObj = std::make_unique(); + curObj->set_name(std::move(key)); + ++curIter; + } + } + // end of new object + else if (curObj && *curIter == TYTI_L(charT, '}')) { + if (!lvls.empty()) { + // get object before + std::unique_ptr prev{std::move(lvls.top())}; + lvls.pop(); + + // add finished obj to obj before and release it from processing + prev->add_child(std::move(curObj)); + curObj = std::move(prev); + } else { + roots.push_back(std::move(curObj)); + curObj.reset(); + } + ++curIter; + } else { + throw std::runtime_error{"unexpected '}'"}; + } + } + if (curObj != nullptr || !lvls.empty()) { + throw std::runtime_error{"object is not closed with '}'"}; + } + + return roots; + } + + } // namespace detail + + /** \brief Read VDF formatted sequences defined by the range [first, last). + If the file is mailformatted, parser will try to read it until it can. + @param first begin iterator + @param end end iterator + + can thow: + - "std::runtime_error" if a parsing error occured + - "std::bad_alloc" if not enough memory coup be allocated + */ + template + OutputT read(IterT first, const IterT last, const Options& opt = Options{}) + { + auto exclude_files = std::unordered_set< + std::basic_string::value_type>>{}; + auto roots = detail::read_internal(first, last, exclude_files, opt); + + OutputT result; + if (roots.size() > 1) { + for (auto& i : roots) + result.add_child(std::move(i)); + } else if (roots.size() == 1) + result = std::move(*roots[0]); + + return result; + } + + /** \brief Read VDF formatted sequences defined by the range [first, last). + If the file is mailformatted, parser will try to read it until it can. + @param first begin iterator + @param end end iterator + @param ec output bool. 0 if ok, otherwise, holds an system error code + + Possible error codes: + std::errc::protocol_error: file is mailformatted + std::errc::not_enough_memory: not enough space + std::errc::invalid_argument: iterators throws e.g. out of range + */ + template + OutputT read(IterT first, IterT last, std::error_code& ec, + const Options& opt = Options{}) NOEXCEPT + + { + ec.clear(); + OutputT r{}; + try { + r = read(first, last, opt); + } catch (std::runtime_error&) { + ec = std::make_error_code(std::errc::protocol_error); + } catch (std::bad_alloc&) { + ec = std::make_error_code(std::errc::not_enough_memory); + } catch (...) { + ec = std::make_error_code(std::errc::invalid_argument); + } + return r; + } + + /** \brief Read VDF formatted sequences defined by the range [first, last). + If the file is mailformatted, parser will try to read it until it can. + @param first begin iterator + @param end end iterator + @param ok output bool. true, if parser successed, false, if parser failed + */ + template + OutputT read(IterT first, const IterT last, bool* ok, + const Options& opt = Options{}) NOEXCEPT + { + std::error_code ec; + auto r = read(first, last, ec, opt); + if (ok) + *ok = !ec; + return r; + } + + template + inline auto read(IterT first, const IterT last, bool* ok, + const Options& opt = Options{}) NOEXCEPT + -> basic_object::value_type> + { + return read::value_type>>( + first, last, ok, opt); + } + + template + inline auto read(IterT first, IterT last, std::error_code& ec, + const Options& opt = Options{}) NOEXCEPT + -> basic_object::value_type> + { + return read::value_type>>( + first, last, ec, opt); + } + + template + inline auto read(IterT first, const IterT last, const Options& opt = Options{}) + -> basic_object::value_type> + { + return read::value_type>>( + first, last, opt); + } + + /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf + formatted data. throws "std::bad_alloc" if file buffer could not be allocated + */ + template + OutputT read(iStreamT& inStream, std::error_code& ec, const Options& opt = Options{}) + { + // cache the file + typedef typename iStreamT::char_type charT; + std::basic_string str = detail::read_file(inStream); + + // parse it + return read(str.begin(), str.end(), ec, opt); + } + + template + inline basic_object + read(iStreamT& inStream, std::error_code& ec, const Options& opt = Options{}) + { + return read>(inStream, ec, opt); + } + + /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf + formatted data. throws "std::bad_alloc" if file buffer could not be allocated ok == + false, if a parsing error occured + */ + template + OutputT read(iStreamT& inStream, bool* ok, const Options& opt = Options{}) + { + std::error_code ec; + const auto r = read(inStream, ec, opt); + if (ok) + *ok = !ec; + return r; + } + + template + inline basic_object read(iStreamT& inStream, bool* ok, + const Options& opt = Options{}) + { + return read>(inStream, ok, opt); + } + + /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf + formatted data. throws "std::bad_alloc" if file buffer could not be allocated + throws "std::runtime_error" if a parsing error occured + */ + template + OutputT read(iStreamT& inStream, const Options& opt) + { + + // cache the file + typedef typename iStreamT::char_type charT; + std::basic_string str = detail::read_file(inStream); + // parse it + return read(str.begin(), str.end(), opt); + } + + template + inline basic_object read(iStreamT& inStream, + const Options& opt = Options{}) + { + return read>(inStream, opt); + } + +} // namespace vdf +} // namespace tyti +#ifndef TYTI_NO_L_UNDEF +#undef TYTI_L +#endif + +#ifdef TYTI_UNDEF_CONSTEXPR +#undef CONSTEXPR +#undef TYTI_NO_L_UNDEF +#endif + +#ifdef TYTI_UNDEF_NOTHROW +#undef NOTHROW +#undef TYTI_UNDEF_NOTHROW +#endif + +#endif //__TYTI_STEAM_VDF_PARSER_H__ diff --git a/libs/game_bethesda/src/games/enderal/CMakeLists.txt b/libs/game_bethesda/src/games/enderal/CMakeLists.txt new file mode 100644 index 0000000..348429c --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_enderal SHARED + enderalbsainvalidation.cpp + enderalbsainvalidation.h + enderaldataarchives.cpp + enderaldataarchives.h + enderalgameplugins.cpp + enderalgameplugins.h + enderallocalsavegames.cpp + enderallocalsavegames.h + enderalmoddatachecker.h + enderalmoddatacontent.h + enderalsavegame.cpp + enderalsavegame.h + enderalscriptextender.cpp + enderalscriptextender.h + gameenderal.cpp + gameenderal.h +) +mo2_configure_plugin(game_enderal NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_enderal PRIVATE game_creation) +mo2_install_plugin(game_enderal) diff --git a/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp new file mode 100644 index 0000000..5564075 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "enderalbsainvalidation.h" + +EnderalBSAInvalidation::EnderalBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "enderal.ini", game) +{} + +QString EnderalBSAInvalidation::invalidationBSAName() const +{ + return "Enderal - Invalidation.bsa"; +} + +unsigned long EnderalBSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h new file mode 100644 index 0000000..721da3e --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h @@ -0,0 +1,20 @@ +#ifndef ENDERALBSAINVALIDATION_H +#define ENDERALBSAINVALIDATION_H + +#include "enderaldataarchives.h" +#include "gamebryobsainvalidation.h" + +#include + +class EnderalBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + EnderalBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; +}; + +#endif // ENDERALBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp b/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp new file mode 100644 index 0000000..3ac4ee0 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp @@ -0,0 +1,42 @@ +#include "enderaldataarchives.h" +#include + +QStringList EnderalDataArchives::vanillaArchives() const +{ + return {"Skyrim - Misc.bsa", "Skyrim - Shaders.bsa", "Skyrim - Textures.bsa", + "Skyrim - Interface.bsa", "Skyrim - Animations.bsa", "Skyrim - Meshes.bsa", + "Skyrim - Sounds.bsa", "E - Meshes.bsa", "E - Music.bsa", + "E - Scripts.bsa", "E - Sounds.bsa", "E - Textures1.bsa", + "E - Textures2.bsa", "E - Textures3.bsa", "L - Textures.bsa", + "L - Voices.bsa"}; +} + +QStringList EnderalDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini") + : localGameDirectory().absoluteFilePath("enderal.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void EnderalDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini") + : localGameDirectory().absoluteFilePath("enderal.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/enderal/enderaldataarchives.h b/libs/game_bethesda/src/games/enderal/enderaldataarchives.h new file mode 100644 index 0000000..cea1a8b --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderaldataarchives.h @@ -0,0 +1,23 @@ +#ifndef ENDERALDATAARCHIVES_H +#define ENDERALDATAARCHIVES_H + +#include +#include +#include +#include +#include + +class EnderalDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // ENDERALDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp b/libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp new file mode 100644 index 0000000..cd8048e --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp @@ -0,0 +1,123 @@ +#include "enderalgameplugins.h" +#include +#include +#include +#include + +#include +#include +#include + +using MOBase::IOrganizer; +using MOBase::IPluginGame; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +EnderalGamePlugins::EnderalGamePlugins(IOrganizer* organizer) + : GamebryoGamePlugins(organizer) +{} + +void EnderalGamePlugins::readPluginLists(MOBase::IPluginList* pluginList) +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (pluginsIsNew && !loadOrderIsNew) { + // If the plugins is new but not loadorder, we must reparse the load order from the + // plugin files + + // removed because returned loadorder was incorrect and did not account for plugins + // that were already disabled before. + /*QStringList loadOrder = readPluginList(pluginList); + pluginList->setLoadOrder(loadOrder);*/ + + // Fix me: we are ignoring order changes in plugins.txt favouring loadorder.txt in + // all cases (plugins.txt should have precedence) + QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); + pluginList->setLoadOrder(loadOrder); + readPluginList(pluginList); + } else { + // read both files if they are both new or both older than the last read + QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); + pluginList->setLoadOrder(loadOrder); + readPluginList(pluginList); + } + + m_LastRead = QDateTime::currentDateTime(); +} + +// TODO: return value is incorrect and should be ignored (it's not currently used +QStringList EnderalGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + QStringList plugins = pluginList->pluginNames(); + QStringList primaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList loadOrder(plugins); + + for (const QString& pluginName : primaryPlugins) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + + // Do not sort the primary plugins. Their load order should be locked as defined in + // "primaryPlugins". + const QStringList pluginsClone(plugins); + for (QString plugin : pluginsClone) { + if (primaryPlugins.contains(plugin, Qt::CaseInsensitive)) + plugins.removeAll(plugin); + } + + // Determine plugin active state by the plugins.txt file. + bool pluginsTxtExists = true; + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + pluginsTxtExists = false; + } + ON_BLOCK_EXIT([&]() { + qDebug("close %s", qUtf8Printable(filePath)); + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + pluginsTxtExists = false; + } + + if (pluginsTxtExists) { + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + pluginName = QStringEncoder(QStringConverter::Encoding::System) + .encode(line.trimmed().constData()); + } + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + plugins.removeAll(pluginName); + // we already have the old loadorder and we ignore the positions in plugins.txt + // (needs fix) loadOrder.append(pluginName); + } + } + + file.close(); + + // we removed each plugin found in the file, so what's left are inactive mods + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } else { + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return loadOrder; +} diff --git a/libs/game_bethesda/src/games/enderal/enderalgameplugins.h b/libs/game_bethesda/src/games/enderal/enderalgameplugins.h new file mode 100644 index 0000000..f78904d --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalgameplugins.h @@ -0,0 +1,23 @@ +#ifndef _ENDERALGAMEPLUGINS_H +#define _ENDERALGAMEPLUGINS_H + +#include +#include +#include +#include + +class EnderalGamePlugins : public GamebryoGamePlugins +{ +public: + EnderalGamePlugins(MOBase::IOrganizer* organizer); + + virtual void readPluginLists(MOBase::IPluginList* pluginList) override; + +protected: + virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override; + +private: + std::map m_LastSaveHash; +}; + +#endif // _ENDERALSEGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp new file mode 100644 index 0000000..e01783a --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp @@ -0,0 +1,24 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "enderallocalsavegames.h" + +QString EnderalLocalSavegames::localSavesDummy() const +{ + return "..\\Enderal\\__MO_Saves\\"; +} diff --git a/libs/game_bethesda/src/games/enderal/enderallocalsavegames.h b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.h new file mode 100644 index 0000000..79a8d43 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.h @@ -0,0 +1,35 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef ENDERALLOCALSAVEGAMES_H +#define ENDERALLOCALSAVEGAMES_H + +#include + +#include + +class EnderalLocalSavegames : public GamebryoLocalSavegames +{ +public: + using GamebryoLocalSavegames::GamebryoLocalSavegames; + +protected: + QString localSavesDummy() const override; +}; + +#endif // ENDERALLOCALSAVEGAMES_H diff --git a/libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h b/libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h new file mode 100644 index 0000000..55f8aac --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h @@ -0,0 +1,49 @@ +#ifndef ENDERAL_MODATACHECKER_H +#define ENDERAL_MODATACHECKER_H + +#include + +class EnderalModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{"fonts", + "interface", + "menus", + "meshes", + "music", + "scripts", + "shaders", + "sound", + "strings", + "textures", + "trees", + "video", + "facegen", + "materials", + "skse", + "distantlod", + "asi", + "Tools", + "MCM", + "distantland", + "mits", + "dllplugins", + "SkyProc Patchers", + "CalienteTools", + "NetScriptFramework", + "shadersfx"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // ENDERAL_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h b/libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h new file mode 100644 index 0000000..47b45b9 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h @@ -0,0 +1,14 @@ +#ifndef ENDERAL_MODDATACONTENT_H +#define ENDERAL_MODDATACONTENT_H + +#include +#include + +// Like Skyrim, Enderal does not need any change from the default feature: +class EnderalModDataContent : public GamebryoModDataContent +{ +public: + using GamebryoModDataContent::GamebryoModDataContent; +}; + +#endif // ENDERAL_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/enderal/enderalsavegame.cpp b/libs/game_bethesda/src/games/enderal/enderalsavegame.cpp new file mode 100644 index 0000000..cd9beb3 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalsavegame.cpp @@ -0,0 +1,101 @@ +#include "enderalsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gameenderal.h" + +EnderalSaveGame::EnderalSaveGame(QString const& fileName, GameEnderal const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); + + FILETIME ftime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + setCreationTime(ctime); +} + +void EnderalSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const +{ + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + + file.read(playerLocation); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); +} + +std::unique_ptr EnderalSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); + std::unique_ptr fields = std::make_unique(); + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + fields->Screenshot = file.readImage(); + + file.skip(); // form version + file.skip(); // plugin info size + + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/enderal/enderalsavegame.h b/libs/game_bethesda/src/games/enderal/enderalsavegame.h new file mode 100644 index 0000000..c29eb46 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalsavegame.h @@ -0,0 +1,23 @@ +#ifndef ENDERALSAVEGAME_H +#define ENDERALSAVEGAME_H + +#include "gamebryosavegame.h" +#include "windows_compat.h" + +class GameEnderal; + +class EnderalSaveGame : public GamebryoSaveGame +{ +public: + EnderalSaveGame(QString const& fileName, GameEnderal const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // ENDERALSAVEGAME_H diff --git a/libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp b/libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp new file mode 100644 index 0000000..ff3864b --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp @@ -0,0 +1,18 @@ +#include "enderalscriptextender.h" + +#include +#include + +EnderalScriptExtender::EnderalScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString EnderalScriptExtender::BinaryName() const +{ + return "skse_loader.exe"; +} + +QString EnderalScriptExtender::PluginPath() const +{ + return "skse/plugins"; +} diff --git a/libs/game_bethesda/src/games/enderal/enderalscriptextender.h b/libs/game_bethesda/src/games/enderal/enderalscriptextender.h new file mode 100644 index 0000000..7d0d027 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalscriptextender.h @@ -0,0 +1,17 @@ +#ifndef ENDERALSCRIPTEXTENDER_H +#define ENDERALSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class EnderalScriptExtender : public GamebryoScriptExtender +{ +public: + EnderalScriptExtender(const GameGamebryo* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // ENDERALSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/enderal/game_enderal_en.ts b/libs/game_bethesda/src/games/enderal/game_enderal_en.ts new file mode 100644 index 0000000..9dcac4d --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/game_enderal_en.ts @@ -0,0 +1,17 @@ + + + + + GameEnderal + + + Enderal Support Plugin + + + + + Adds support for the game Enderal + + + + diff --git a/libs/game_bethesda/src/games/enderal/gameenderal.cpp b/libs/game_bethesda/src/games/enderal/gameenderal.cpp new file mode 100644 index 0000000..fb4e1d7 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/gameenderal.cpp @@ -0,0 +1,267 @@ +#include "gameenderal.h" + +#include "enderalbsainvalidation.h" +#include "enderaldataarchives.h" +#include "enderalgameplugins.h" +#include "enderallocalsavegames.h" +#include "enderalmoddatachecker.h" +#include "enderalmoddatacontent.h" +#include "enderalsavegame.h" +#include "enderalscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "steamutility.h" +#include "utility.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#ifdef _WIN32 +#include +#include +#endif + +#include +#include +#include +#include +#include + +using namespace MOBase; + +GameEnderal::GameEnderal() {} + +bool GameEnderal::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "enderal.ini")); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameEnderal::gameName() const +{ + return "Enderal"; +} + +QList GameEnderal::executables() const +{ + return QList() + //<< ExecutableInfo("SKSE", + // findInGameFolder(feature()->loaderName())) + //<< ExecutableInfo("SBW", findInGameFolder("SBW.exe")) + << ExecutableInfo("Enderal (SKSE)", findInGameFolder(binaryName())) + << ExecutableInfo("Enderal Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Enderal\"") + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("202480"); +} + +QList GameEnderal::executableForcedLoads() const +{ + return QList(); +} + +QString GameEnderal::name() const +{ + return "Enderal Support Plugin"; +} + +QString GameEnderal::localizedName() const +{ + return tr("Enderal Support Plugin"); +} + +QString GameEnderal::author() const +{ + return "AL12 & MO2 Team"; +} + +QString GameEnderal::description() const +{ + return tr("Adds support for the game Enderal"); +} + +MOBase::VersionInfo GameEnderal::version() const +{ + return VersionInfo(1, 3, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameEnderal::settings() const +{ + QList results; + results.push_back( + PluginSetting("sse_downloads", "allow Skyrim SE downloads", QVariant(false))); + return results; +} + +void GameEnderal::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/enderal", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/enderal.ini").exists()) { + + // there is no default ini, actually they are going to put them in for us! + copyToProfile(gameDirectory().absolutePath(), path, "enderal_default.ini", + "enderal.ini"); + copyToProfile(gameDirectory().absolutePath(), path, "enderalprefs_default.ini", + "enderalprefs.ini"); + } else { + copyToProfile(myGamesPath(), path, "enderal.ini"); + copyToProfile(myGamesPath(), path, "enderalprefs.ini"); + } + } +} + +bool GameEnderal::looksValid(QDir const& path) const +{ + // we need to check both launcher and binary because the binary also exists for + // Skyrim and the launcher for Enderal SE + return path.exists(getLauncherName()) && path.exists(binaryName()); +} + +QIcon GameEnderal::gameIcon() const +{ + return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(getLauncherName())); +} + +QString GameEnderal::savegameExtension() const +{ + return "ess"; +} + +QString GameEnderal::savegameSEExtension() const +{ + return "skse"; +} + +std::shared_ptr +GameEnderal::makeSaveGame(QString filepath) const +{ + return std::make_shared(filepath, this); +} + +QString GameEnderal::steamAPPId() const +{ + return "933480"; +} + +QStringList GameEnderal::primaryPlugins() const +{ + return {"Skyrim.esm", "Enderal - Forgotten Stories.esm", "Update.esm"}; +} + +QString GameEnderal::binaryName() const +{ + return "skse_loader.exe"; +} + +QString GameEnderal::getLauncherName() const +{ + return "Enderal Launcher.exe"; +} + +QString GameEnderal::gameShortName() const +{ + return "Enderal"; +} + +QString GameEnderal::gameNexusName() const +{ + return "enderal"; +} + +QStringList GameEnderal::primarySources() const +{ + return {"Skyrim"}; +} + +QStringList GameEnderal::validShortNames() const +{ + QStringList results; + results.push_back("Skyrim"); + if (m_Organizer->pluginSetting(name(), "sse_downloads").toBool()) { + results.push_back("SkyrimSE"); + } + return results; +} + +QStringList GameEnderal::iniFiles() const +{ + return {"enderal.ini", "enderalprefs.ini"}; +} + +QStringList GameEnderal::DLCPlugins() const +{ + return {}; +} + +IPluginGame::LoadOrderMechanism GameEnderal::loadOrderMechanism() const +{ + return LoadOrderMechanism::PluginsTxt; +} + +int GameEnderal::nexusModOrganizerID() const +{ + return 0; +} + +int GameEnderal::nexusGameID() const +{ + return 2736; +} + +QString GameEnderal::identifyGamePath() const +{ +#ifdef _WIN32 + QString path = "Software\\SureAI\\Enderal"; + QString result; + try { + result = + findInRegistry(HKEY_CURRENT_USER, path.toStdWString().c_str(), L"Install_Path"); + } catch (MOBase::MyException) { + result = MOBase::findSteamGame("Enderal", "Data\\Enderal - Forgotten Stories.esm"); + } + return result; +#else + // Prefer exact Steam app-id resolution for Enderal LE. + QString result = parseSteamLocation(steamAPPId(), "Enderal"); + if (!result.isEmpty() && looksValid(QDir(result))) { + return result; + } + + result = MOBase::findSteamGame("Enderal", "Data/Enderal - Forgotten Stories.esm"); + if (!result.isEmpty() && looksValid(QDir(result))) { + return result; + } + + return GameGamebryo::identifyGamePath(); +#endif +} diff --git a/libs/game_bethesda/src/games/enderal/gameenderal.h b/libs/game_bethesda/src/games/enderal/gameenderal.h new file mode 100644 index 0000000..fb0a377 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/gameenderal.h @@ -0,0 +1,61 @@ +#ifndef GAMEENDERAL_H +#define GAMEENDERAL_H + +#include "gamegamebryo.h" + +#include +#include + +class GameEnderal : public GameGamebryo +{ + Q_OBJECT +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.GameEnderal" FILE "gameenderal.json") +#endif + +public: + GameEnderal(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QString binaryName() const override; + virtual QString getLauncherName() const override; + virtual QString gameShortName() const override; + virtual QStringList validShortNames() const override; + virtual QString gameNexusName() const override; + virtual QStringList primarySources() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + virtual bool looksValid(QDir const&) const override; + virtual QIcon gameIcon() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +protected: + virtual QString savegameExtension() const override; + virtual QString savegameSEExtension() const override; + virtual std::shared_ptr + makeSaveGame(QString filepath) const override; + + virtual QString identifyGamePath() const override; +}; + +#endif // GAMEENDERAL_H diff --git a/libs/game_bethesda/src/games/enderal/gameenderal.json b/libs/game_bethesda/src/games/enderal/gameenderal.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/gameenderal.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/enderalse/CMakeLists.txt b/libs/game_bethesda/src/games/enderalse/CMakeLists.txt new file mode 100644 index 0000000..1e33fbf --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_enderalse SHARED + enderalsedataarchives.cpp + enderalsedataarchives.h + enderalsegameplugins.cpp + enderalsegameplugins.h + enderalselocalsavegames.cpp + enderalselocalsavegames.h + enderalsemoddatachecker.h + enderalsemoddatacontent.h + enderalsesavegame.cpp + enderalsesavegame.h + enderalsescriptextender.cpp + enderalsescriptextender.h + enderalseunmanagedmods.cpp + enderalseunmanagedmods.h + gameenderalse.cpp + gameenderalse.h +) +mo2_configure_plugin(game_enderalse NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_enderalse PRIVATE game_creation) +mo2_install_plugin(game_enderalse) diff --git a/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp new file mode 100644 index 0000000..606c688 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp @@ -0,0 +1,64 @@ +#include "enderalsedataarchives.h" + +#include "iprofile.h" +#include + +QStringList EnderalSEDataArchives::vanillaArchives() const +{ + return {"Skyrim - Textures0.bsa", + "Skyrim - Textures1.bsa", + "Skyrim - Textures2.bsa", + "Skyrim - Textures3.bsa", + "Skyrim - Textures4.bsa", + "Skyrim - Textures5.bsa", + "Skyrim - Textures6.bsa", + "Skyrim - Textures7.bsa", + "Skyrim - Textures8.bsa", + "Skyrim - Meshes0.bsa", + "Skyrim - Meshes1.bsa", + "Skyrim - Voices_en0.bsa", + "Skyrim - Sounds.bsa", + "Skyrim - Interface.bsa", + "Skyrim - Animations.bsa", + "Skyrim - Shaders.bsa", + "Skyrim - Misc.bsa", + "E - Meshes.bsa", + "E - SE.bsa", + "E - Scripts.bsa", + "E - Sounds.bsa", + "E - Textures1.bsa", + "E - Textures2.bsa", + "E - Textures3.bsa", + "L - Textures.bsa", + "L - Voices.bsa"}; +} + +QStringList EnderalSEDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini") + : localGameDirectory().absoluteFilePath("enderal.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList", 512)); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2", 512)); + + return result; +} + +void EnderalSEDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(","); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini") + : localGameDirectory().absoluteFilePath("enderal.ini"); + if (list.length() > 511) { + int splitIdx = list.lastIndexOf(",", 512); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.h b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.h new file mode 100644 index 0000000..4c51e93 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.h @@ -0,0 +1,26 @@ +#ifndef ENDERALSEDATAARCHIVES_H +#define ENDERALSEDATAARCHIVES_H + +#include "gamebryodataarchives.h" +#include +#include + +namespace MOBase +{ +class IProfile; +} + +class EnderalSEDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // _SKYRIMSEDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp new file mode 100644 index 0000000..d7be8ce --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp @@ -0,0 +1,82 @@ +#include "enderalsegameplugins.h" + +#include +#include +#include +#include + +#include + +using namespace MOBase; + +void EnderalSEGamePlugins::writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) +{ + SafeWriteFile file(filePath); + + QStringEncoder encoder(QStringConverter::Encoding::System); + + file->resize(0); + + file->write( + encoder.encode("# This file was automatically generated by Mod Organizer.\r\n")); + + bool invalidFileNames = false; + int writtenCount = 0; + + QStringList plugins = pluginList->pluginNames(); + std::sort(plugins.begin(), plugins.end(), + [pluginList](const QString& lhs, const QString& rhs) { + return pluginList->priority(lhs) < pluginList->priority(rhs); + }); + + QStringList PrimaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList DLCPlugins = organizer()->managedGame()->DLCPlugins(); + QSet ManagedMods = + QSet(PrimaryPlugins.begin(), PrimaryPlugins.end()); + QSet DLCSet = QSet(DLCPlugins.begin(), DLCPlugins.end()); + ManagedMods.subtract(DLCSet); + PrimaryPlugins.append(QList(ManagedMods.begin(), ManagedMods.end())); + + // we need to force some plugins because those are not force-loaded + // by the game but are considered primary plugins for users + file->write("*Enderal - Forgotten Stories.esm\r\n"); + file->write("*SkyUI_SE.esp\r\n"); + + // TODO: do not write plugins in OFFICIAL_FILES container + for (const QString& pluginName : plugins) { + if (!PrimaryPlugins.contains(pluginName, Qt::CaseInsensitive)) { + if (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE) { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write("*"); + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } else { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } + } + } + + if (invalidFileNames) { + reportError(QObject::tr("Some of your plugins have invalid names! These " + "plugins can not be loaded by the game. Please see " + "mo_interface.log for a list of affected plugins " + "and rename them.")); + } + + file->commit(); +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h new file mode 100644 index 0000000..700a074 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h @@ -0,0 +1,19 @@ +#ifndef ENDERALSEGAMEPLUGINS_H +#define ENDERALSEGAMEPLUGINS_H + +#include +#include +#include +#include + +class EnderalSEGamePlugins : public CreationGamePlugins +{ +public: + using CreationGamePlugins::CreationGamePlugins; + +protected: + void writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) override; +}; + +#endif // ENDERALSEGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp new file mode 100644 index 0000000..fafa739 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp @@ -0,0 +1,24 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "enderalselocalsavegames.h" + +QString EnderalSELocalSavegames::localSavesDummy() const +{ + return "..\\Enderal Special Edition\\__MO_Saves\\"; +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h new file mode 100644 index 0000000..f0d14ad --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h @@ -0,0 +1,17 @@ +#ifndef ENDERALSELOCALSAVEGAMES_H +#define ENDERALSELOCALSAVEGAMES_H + +#include + +#include + +class EnderalSELocalSavegames : public GamebryoLocalSavegames +{ +public: + using GamebryoLocalSavegames::GamebryoLocalSavegames; + +protected: + QString localSavesDummy() const override; +}; + +#endif // ENDERALSELOCALSAVEGAMES_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h b/libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h new file mode 100644 index 0000000..2336819 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h @@ -0,0 +1,31 @@ +#ifndef ENDERALSE_MODATACHECKER_H +#define ENDERALSE_MODATACHECKER_H + +#include + +class EnderalSEModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + "fonts", "interface", "menus", "meshes", + "music", "scripts", "shaders", "sound", + "strings", "textures", "trees", "video", + "facegen", "materials", "skse", "distantlod", + "asi", "Tools", "MCM", "distantland", + "mits", "dllplugins", "CalienteTools", "NetScriptFramework", + "shadersfx", "Nemesis_Engine"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "esl", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // SKYRIMSE_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h b/libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h new file mode 100644 index 0000000..a9cce03 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h @@ -0,0 +1,21 @@ +#ifndef ENDERALSE_MODDATACONTENT_H +#define ENDERALSE_MODDATACONTENT_H + +#include +#include + +class EnderalSEModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + EnderalSEModDataContent(MOBase::IGameFeatures* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_SKYPROC] = false; + } +}; + +#endif // SKYRIMSE_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp new file mode 100644 index 0000000..2662505 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp @@ -0,0 +1,148 @@ +#include "enderalsesavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +union _ULARGE_INTEGER { + struct { + uint32_t LowPart; + uint32_t HighPart; + }; + uint64_t QuadPart; +}; + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +EnderalSESaveGame::EnderalSESaveGame(QString const& fileName, GameEnderalSE const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(fileName, "TESV_SAVEGAME"); // 10bytes + + uint32_t version; + FILETIME ftime; + fetchInformationFields(file, version, m_PCName, m_PCLevel, m_PCLocation, m_SaveNumber, + ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + + // For some reason, the file time is off by about 6 hours. + // So we need to subtract those 6 hours from the filetime. + _ULARGE_INTEGER time; + time.LowPart = ftime.dwLowDateTime; + time.HighPart = ftime.dwHighDateTime; + time.QuadPart -= 2.16e11; + ftime.dwHighDateTime = time.HighPart; + ftime.dwLowDateTime = time.LowPart; + + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + + setCreationTime(ctime); +} + +void EnderalSESaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& version, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, uint32_t& saveNumber, + FILETIME& creationTime) const +{ + uint32_t headerSize; + file.read(headerSize); // header size "TESV_SAVEGAME" + file.read(version); + file.read(saveNumber); + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); // filetime +} + +std::unique_ptr EnderalSESaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); // 10bytes + + uint32_t version = 0; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, version, dummyName, dummyLevel, dummyLocation, + dummySaveNumber, dummyTime); + } + + std::unique_ptr fields = std::make_unique(); + + uint32_t width; + uint32_t height; + file.read(width); + file.read(height); + + bool alpha = false; + + // compatibility between LE and SE: + // SE has an additional uin16_t for compression + // SE uses an alpha channel, whereas LE does not + if (version == 12) { + uint16_t compressionType; + file.read(compressionType); + file.setCompressionType(compressionType); + alpha = true; + } + + fields->Screenshot = file.readImage(width, height, 320, alpha); + + file.openCompressedData(); + + uint8_t saveGameVersion = file.readChar(); + uint8_t pluginInfoSize = file.readChar(); + uint16_t other = file.readShort(); // Unknown + + fields->Plugins = file.readPlugins(1); // Just empty data + + if (saveGameVersion >= 78) { + fields->LightPlugins = file.readLightPlugins(); + } + + file.closeCompressedData(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalsesavegame.h b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.h new file mode 100644 index 0000000..1363142 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.h @@ -0,0 +1,28 @@ +#ifndef ENDERALSESAVEGAME_H +#define ENDERALSESAVEGAME_H + +#include "gamebryosavegame.h" +#include "gameenderalse.h" +#include "windows_compat.h" + +namespace MOBase +{ +class IPluginGame; +} + +class EnderalSESaveGame : public GamebryoSaveGame +{ +public: + EnderalSESaveGame(QString const& fileName, GameEnderalSE const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& version, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, uint32_t& saveNumber, + FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // _SKYRIMSESAVEGAME_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp new file mode 100644 index 0000000..f7a3113 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp @@ -0,0 +1,18 @@ +#include "enderalsescriptextender.h" + +#include +#include + +EnderalSEScriptExtender::EnderalSEScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString EnderalSEScriptExtender::BinaryName() const +{ + return "skse64_loader.exe"; +} + +QString EnderalSEScriptExtender::PluginPath() const +{ + return "skse/plugins"; +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h new file mode 100644 index 0000000..79edaf2 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h @@ -0,0 +1,17 @@ +#ifndef ENDERALSESCRIPTEXTENDER_H +#define ENDERALESCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class EnderalSEScriptExtender : public GamebryoScriptExtender +{ +public: + EnderalSEScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // _SKYRIMSESCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp new file mode 100644 index 0000000..9f11b44 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp @@ -0,0 +1,29 @@ +#include "enderalseunmanagedmods.h" + +EnderalSEUnmangedMods::EnderalSEUnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +EnderalSEUnmangedMods::~EnderalSEUnmangedMods() {} + +QStringList EnderalSEUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h new file mode 100644 index 0000000..6eda502 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h @@ -0,0 +1,16 @@ +#ifndef ENDERALSEUNMANAGEDMODS_H +#define ENDERALSEUNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include + +class EnderalSEUnmangedMods : public GamebryoUnmangedMods +{ +public: + EnderalSEUnmangedMods(const GameGamebryo* game); + ~EnderalSEUnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; +}; + +#endif // _SKYRIMSEUNMANAGEDMODS_H diff --git a/libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts b/libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts new file mode 100644 index 0000000..4c8b5a2 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts @@ -0,0 +1,25 @@ + + + + + GameEnderalSE + + + Enderal Special Edition Support Plugin + + + + + Adds support for the game Enderal Special Edition. + + + + + QObject + + + Some of your plugins have invalid names! These plugins can not be loaded by the game. Please see mo_interface.log for a list of affected plugins and rename them. + + + + diff --git a/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp b/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp new file mode 100644 index 0000000..26587b8 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp @@ -0,0 +1,347 @@ +#include "gameenderalse.h" + +#include "enderalsedataarchives.h" +#include "enderalsegameplugins.h" +#include "enderalselocalsavegames.h" +#include "enderalsemoddatachecker.h" +#include "enderalsemoddatacontent.h" +#include "enderalsesavegame.h" +#include "enderalsescriptextender.h" +#include "enderalseunmanagedmods.h" +#include "steamutility.h" + +#include "versioninfo.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "scopeguard.h" +#include + +using namespace MOBase; + +GameEnderalSE::GameEnderalSE() {} + +void GameEnderalSE::setVariant(QString variant) +{ + m_GameVariant = variant; +} + +void GameEnderalSE::checkVariants() +{ + QFileInfo gog_dll(m_GamePath + "\\Galaxy64.dll"); + if (gog_dll.exists()) + setVariant("GOG"); + else + setVariant("Steam"); +} + +QDir GameEnderalSE::documentsDirectory() const +{ + return m_MyGamesPath; +} + +void GameEnderalSE::detectGame() +{ + m_GamePath = identifyGamePath(); + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QString GameEnderalSE::identifyGamePath() const +{ +#ifdef _WIN32 + QMap paths = { + {"Software\\Bethesda Softworks\\" + gameName(), "Installed Path"}, + {"Software\\GOG.com\\Games\\1708684988", "path"}, + }; + QString result; + try { + for (auto& path : paths.toStdMap()) { + result = findInRegistry(HKEY_LOCAL_MACHINE, path.first.toStdWString().c_str(), + path.second.toStdWString().c_str()); + if (!result.isEmpty()) + break; + } + } catch (MOBase::MyException) { + result = MOBase::findSteamGame("Enderal Special Edition", + "Data\\Enderal - Forgotten Stories.esm"); + } + return result; +#else + // Prefer exact Steam app-id resolution for Enderal SE. + QString result = parseSteamLocation("976620", "Enderal Special Edition"); + if (!result.isEmpty() && looksValid(QDir(result))) { + return result; + } + + result = MOBase::findSteamGame("Enderal Special Edition", + "Data/Enderal - Forgotten Stories.esm"); + if (!result.isEmpty() && looksValid(QDir(result))) { + return result; + } + + return GameGamebryo::identifyGamePath(); +#endif +} + +void GameEnderalSE::setGamePath(const QString& path) +{ + m_GamePath = path; + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QDir GameEnderalSE::savesDirectory() const +{ + return QDir(m_MyGamesPath + "/Saves"); +} + +bool GameEnderalSE::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +bool GameEnderalSE::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(this, "enderal.ini")); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameEnderalSE::gameName() const +{ + return "Enderal Special Edition"; +} + +QString GameEnderalSE::gameDirectoryName() const +{ + if (selectedVariant() == "GOG") + return "Enderal Special Edition GOG"; + else + return "Enderal Special Edition"; +} + +QIcon GameEnderalSE::gameIcon() const +{ + return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(getLauncherName())); +} + +QList GameEnderalSE::executables() const +{ + return QList() + << ExecutableInfo("Enderal Special Edition (SKSE)", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Enderal Special Edition Launcher", + findInGameFolder(getLauncherName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("1946180") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Enderal Special Edition\""); +} + +QList GameEnderalSE::executableForcedLoads() const +{ + return QList(); +} + +QString GameEnderalSE::binaryName() const +{ + return "skse64_loader.exe"; +} + +QString GameEnderalSE::getLauncherName() const +{ + return "Enderal Launcher.exe"; +} + +bool GameEnderalSE::looksValid(const QDir& folder) const +{ + // we need to check both launcher and binary because the binary also exists for + // Skyrim SE and the launcher for Enderal LE + return folder.exists(getLauncherName()) && folder.exists(binaryName()); +} + +QFileInfo GameEnderalSE::findInGameFolder(const QString& relativePath) const +{ + return QFileInfo(m_GamePath + "/" + relativePath); +} + +QString GameEnderalSE::name() const +{ + return "Enderal Special Edition Support Plugin"; +} + +QString GameEnderalSE::localizedName() const +{ + return tr("Enderal Special Edition Support Plugin"); +} + +QString GameEnderalSE::author() const +{ + return "Archost, ZachHaber & MO2 Team"; +} + +QString GameEnderalSE::description() const +{ + return tr("Adds support for the game Enderal Special Edition."); +} + +MOBase::VersionInfo GameEnderalSE::version() const +{ + return VersionInfo(1, 2, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameEnderalSE::settings() const +{ + return QList(); +} + +void GameEnderalSE::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + gameDirectoryName(), path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/Enderal.ini").exists()) { + + // there is no default ini, actually they are going to put them in for us! + copyToProfile(gameDirectory().absolutePath(), path, "enderal_default.ini", + "Enderal.ini"); + copyToProfile(gameDirectory().absolutePath(), path, "enderalprefs_default.ini", + "EnderalPrefs.ini"); + } else { + copyToProfile(myGamesPath(), path, "Enderal.ini"); + copyToProfile(myGamesPath(), path, "EnderalPrefs.ini"); + } + } +} + +QString GameEnderalSE::savegameExtension() const +{ + return "ess"; +} + +QString GameEnderalSE::savegameSEExtension() const +{ + return "skse"; +} + +std::shared_ptr +GameEnderalSE::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameEnderalSE::steamAPPId() const +{ + if (selectedVariant() == "Steam") + return "976620"; + return ""; +} + +QStringList GameEnderalSE::primaryPlugins() const +{ + return {"skyrim.esm", "update.esm", "dawnguard.esm", "hearthfires.esm", + "dragonborn.esm", + + // these two plugins are considered "primary" for users but are not + // automatically loaded by the game so we need to force-write them + // to the plugin list + "enderal - forgotten stories.esm", "skyui_se.esp"}; +} + +QStringList GameEnderalSE::DLCPlugins() const +{ + return {}; +} + +QStringList GameEnderalSE::gameVariants() const +{ + return {"Steam", "GOG"}; +} + +QString GameEnderalSE::gameShortName() const +{ + return "EnderalSE"; +} + +QStringList GameEnderalSE::validShortNames() const +{ + return {"Skyrim", "SkyrimSE", "Enderal"}; +} + +QString GameEnderalSE::gameNexusName() const +{ + return "enderalspecialedition"; +} + +QStringList GameEnderalSE::iniFiles() const +{ + return {"Enderal.ini", "EnderalPrefs.ini"}; +} + +QStringList GameEnderalSE::CCPlugins() const +{ + return {}; +} + +IPluginGame::LoadOrderMechanism GameEnderalSE::loadOrderMechanism() const +{ + return IPluginGame::LoadOrderMechanism::PluginsTxt; +} + +int GameEnderalSE::nexusModOrganizerID() const +{ + return 0; +} + +int GameEnderalSE::nexusGameID() const +{ + return 3685; +} + +QDir GameEnderalSE::gameDirectory() const +{ + return QDir(m_GamePath); +} + +// Not to delete all the spaces... +MappingType GameEnderalSE::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameDirectoryName() + "/" + profileFile, + false}); + } + + return result; +} diff --git a/libs/game_bethesda/src/games/enderalse/gameenderalse.h b/libs/game_bethesda/src/games/enderalse/gameenderalse.h new file mode 100644 index 0000000..754951d --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/gameenderalse.h @@ -0,0 +1,75 @@ +#ifndef GAMEENDERALSE_H +#define GAMEENDERALSE_H + +#include "gamegamebryo.h" + +#include +#include + +class GameEnderalSE : public GameGamebryo +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "com.soundcontactstudio.GameEnderalSE" FILE + "gameenderalse.json") + +public: + GameEnderalSE(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual void detectGame() override; + virtual QString gameName() const override; + virtual QIcon gameIcon() const override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString binaryName() const override; + virtual QString getLauncherName() const override; + virtual bool looksValid(const QDir& folder) const override; + virtual QString gameShortName() const override; + virtual QString gameNexusName() const override; + virtual QStringList validShortNames() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual QStringList CCPlugins() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + + virtual bool isInstalled() const override; + virtual void setGamePath(const QString& path) override; + virtual QDir gameDirectory() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + virtual MappingType mappings() const override; + +protected: + std::shared_ptr makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; + + QString gameDirectoryName() const; + QDir documentsDirectory() const; + QDir savesDirectory() const; + QFileInfo findInGameFolder(const QString& relativePath) const; + + void checkVariants(); + void setVariant(QString variant); + + virtual QString identifyGamePath() const override; +}; + +#endif // _GAMESKYRIMSE_H diff --git a/libs/game_bethesda/src/games/enderalse/gameenderalse.json b/libs/game_bethesda/src/games/enderalse/gameenderalse.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/gameenderalse.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/fallout3/CMakeLists.txt b/libs/game_bethesda/src/games/fallout3/CMakeLists.txt new file mode 100644 index 0000000..3db9758 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_fallout3 SHARED + fallout3bsainvalidation.cpp + fallout3bsainvalidation.h + fallout3dataarchives.cpp + fallout3dataarchives.h + fallout3moddatachecker.h + fallout3moddatacontent.h + fallout3savegame.cpp + fallout3savegame.h + fallout3scriptextender.cpp + fallout3scriptextender.h + gamefallout3.cpp + gamefallout3.h +) +mo2_configure_plugin(game_fallout3 NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_fallout3 PRIVATE game_gamebryo) +mo2_install_plugin(game_fallout3) diff --git a/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp b/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp new file mode 100644 index 0000000..ce981fb --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "fallout3bsainvalidation.h" + +Fallout3BSAInvalidation::Fallout3BSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "fallout.ini", game) +{} + +QString Fallout3BSAInvalidation::invalidationBSAName() const +{ + return "Fallout - Invalidation.bsa"; +} + +unsigned long Fallout3BSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.h b/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.h new file mode 100644 index 0000000..f836073 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.h @@ -0,0 +1,20 @@ +#ifndef FALLOUT3BSAINVALIDATION_H +#define FALLOUT3BSAINVALIDATION_H + +#include "fallout3dataarchives.h" +#include "gamebryobsainvalidation.h" + +#include + +class Fallout3BSAInvalidation : public GamebryoBSAInvalidation +{ +public: + Fallout3BSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; +}; + +#endif // FALLOUT3BSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp b/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp new file mode 100644 index 0000000..ed6627b --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp @@ -0,0 +1,33 @@ +#include "fallout3dataarchives.h" + +#include "iprofile.h" +#include + +QStringList Fallout3DataArchives::vanillaArchives() const +{ + return {"Fallout - Textures.bsa", "Fallout - Meshes.bsa", "Fallout - Voices.bsa", + "Fallout - Sound.bsa", "Fallout - MenuVoices.bsa", "Fallout - Misc.bsa"}; +} + +QStringList Fallout3DataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + result.append(getArchivesFromKey(iniFile, "SArchiveList")); + + return result; +} + +void Fallout3DataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + setArchivesToKey(iniFile, "SArchiveList", list); +} diff --git a/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.h b/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.h new file mode 100644 index 0000000..8ba4561 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.h @@ -0,0 +1,20 @@ +#ifndef FALLOUT3DATAARCHIVES_H +#define FALLOUT3DATAARCHIVES_H + +#include "gamebryodataarchives.h" +#include + +class Fallout3DataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // FALLOUT3DATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/fallout3/fallout3moddatachecker.h b/libs/game_bethesda/src/games/fallout3/fallout3moddatachecker.h new file mode 100644 index 0000000..0051137 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3moddatachecker.h @@ -0,0 +1,31 @@ +#ifndef FALLOUT3_MODATACHECKER_H +#define FALLOUT3_MODATACHECKER_H + +#include + +class Fallout3ModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + "fonts", "interface", "menus", "meshes", + "music", "scripts", "shaders", "sound", + "strings", "textures", "trees", "video", + "facegen", "materials", "fose", "distantlod", + "asi", "Tools", "MCM", "distantland", + "mits", "dllplugins", "CalienteTools", "NetScriptFramework", + "shadersfx"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // FALLOUT3_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/fallout3/fallout3moddatacontent.h b/libs/game_bethesda/src/games/fallout3/fallout3moddatacontent.h new file mode 100644 index 0000000..bc314a9 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3moddatacontent.h @@ -0,0 +1,22 @@ +#ifndef FALLOUT3_MODDATACONTENT_H +#define FALLOUT3_MODDATACONTENT_H + +#include +#include + +class Fallout3ModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + Fallout3ModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_MCM] = false; + m_Enabled[CONTENT_SKYPROC] = false; + } +}; + +#endif // FALLOUT3_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp b/libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp new file mode 100644 index 0000000..aa9bcde --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp @@ -0,0 +1,72 @@ +#include "fallout3savegame.h" + +#include "gamefallout3.h" + +Fallout3SaveGame::Fallout3SaveGame(QString const& fileName, GameFallout3 const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + + uint32_t width, height; + fetchInformationFields(file, width, height, m_SaveNumber, m_PCName, m_PCLevel, + m_PCLocation); +} + +void Fallout3SaveGame::fetchInformationFields(FileWrapper& file, uint32_t& width, + uint32_t& height, + uint32_t& saveNumber, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation) const +{ + file.skip(); // Save header size + + file.setHasFieldMarkers(true); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BZSTRING); + + file.skip(); // File version ? + file.skip(); // delimiter + + file.read(width); + file.read(height); + file.read(saveNumber); + file.read(playerName); + + QString whatthis; + file.read(whatthis); + + long level; + file.read(level); + playerLevel = level; + + file.read(playerLocation); +} + +std::unique_ptr Fallout3SaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + + std::unique_ptr fields = std::make_unique(); + + uint32_t width, height; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + + fetchInformationFields(file, width, height, dummySaveNumber, dummyName, dummyLevel, + dummyLocation); + } + + QString playtime; + file.read(playtime); + + fields->Screenshot = file.readImage(width, height, 256); + + file.skip(5); // unknown (1 byte), plugin size (4 bytes) + + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/fallout3/fallout3savegame.h b/libs/game_bethesda/src/games/fallout3/fallout3savegame.h new file mode 100644 index 0000000..2a33037 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3savegame.h @@ -0,0 +1,23 @@ +#ifndef FALLOUT3SAVEGAME_H +#define FALLOUT3SAVEGAME_H + +#include "gamebryosavegame.h" + +class GameFallout3; + +class Fallout3SaveGame : public GamebryoSaveGame +{ +public: + Fallout3SaveGame(QString const& fileName, GameFallout3 const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& width, + uint32_t& height, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // FALLOUT3SAVEGAME_H diff --git a/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp b/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp new file mode 100644 index 0000000..608a305 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp @@ -0,0 +1,18 @@ +#include "fallout3scriptextender.h" + +#include +#include + +Fallout3ScriptExtender::Fallout3ScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString Fallout3ScriptExtender::BinaryName() const +{ + return "fose_loader.exe"; +} + +QString Fallout3ScriptExtender::PluginPath() const +{ + return "fose/plugins"; +} diff --git a/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.h b/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.h new file mode 100644 index 0000000..1769d6b --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.h @@ -0,0 +1,17 @@ +#ifndef FALLOUT3SCRIPTEXTENDER_H +#define FALLOUT3SCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class Fallout3ScriptExtender : public GamebryoScriptExtender +{ +public: + Fallout3ScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // FALLOUT3SCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/fallout3/game_fallout3_en.ts b/libs/game_bethesda/src/games/fallout3/game_fallout3_en.ts new file mode 100644 index 0000000..5b90534 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/game_fallout3_en.ts @@ -0,0 +1,17 @@ + + + + + GameFallout3 + + + Fallout 3 Support Plugin + + + + + Adds support for the game Fallout 3. + + + + diff --git a/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp b/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp new file mode 100644 index 0000000..f2f4562 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp @@ -0,0 +1,279 @@ +#include "gamefallout3.h" + +#include "fallout3bsainvalidation.h" +#include "fallout3dataarchives.h" +#include "fallout3moddatachecker.h" +#include "fallout3moddatacontent.h" +#include "fallout3savegame.h" +#include "fallout3scriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace MOBase; + +GameFallout3::GameFallout3() {} + +bool GameFallout3::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "fallout.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + return true; +} + +QString GameFallout3::identifyVariant() const +{ + if (QFile::exists(m_GamePath + "/Fallout3ng.exe")) { + return "Low Violence"; + } else if (QFile::exists(m_GamePath + "/Galaxy.dll")) { + return "GOG"; + } else if (QFile::exists(m_GamePath + "/FalloutLauncherEpic.exe")) { + return "Epic Games"; + } else if (m_GamePath.endsWith("Fallout 3 goty")) { + return "Steam (Game of the Year)"; + } else { + return "Steam (Regular)"; + } +} + +QString GameFallout3::identifyGamePath() const +{ + // Steam (Regular) + auto result = parseSteamLocation("22300", "Fallout 3"); + + // Steam (Game of the Year) + if (result.isEmpty()) { + result = parseSteamLocation("22370", "Fallout 3 goty"); + } + + // Epic Games + if (result.isEmpty()) { + // Fallout 3: Game of the Year Edition: adeae8bbfc94427db57c7dfecce3f1d4 + result = parseEpicGamesLocation({"adeae8bbfc94427db57c7dfecce3f1d4"}); + if (QFileInfo(result).isDir()) { + QDir startPath = QDir(result); + auto subDirs = + startPath.entryList({"Fallout 3 GOTY*"}, QDir::Dirs | QDir::NoDotAndDotDot); + if (!subDirs.isEmpty()) + result = startPath.absoluteFilePath(subDirs.first()); + } + } + + // GOG (and Steam) + if (result.isEmpty()) { + result = GameGamebryo::identifyGamePath(); + } + + return result; +} + +QString GameFallout3::gameName() const +{ + return "Fallout 3"; +} + +void GameFallout3::detectGame() +{ + m_GamePath = identifyGamePath(); + setGameVariant(identifyVariant()); + m_MyGamesPath = determineMyGamesPath("Fallout3"); +} + +QList GameFallout3::executables() const +{ + return QList() + << ExecutableInfo("FOSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Fallout 3", findInGameFolder(binaryName())) + << ExecutableInfo("Fallout Mod Manager", findInGameFolder("fomm/fomm.exe")) + << ExecutableInfo("Construction Kit", findInGameFolder("geck.exe")) + << ExecutableInfo("Fallout Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Fallout3\""); +} + +QList GameFallout3::executableForcedLoads() const +{ + return QList(); +} + +QString GameFallout3::name() const +{ + return "Fallout 3 Support Plugin"; +} + +QString GameFallout3::localizedName() const +{ + return tr("Fallout 3 Support Plugin"); +} + +QString GameFallout3::author() const +{ + return "Tannin"; +} + +QString GameFallout3::description() const +{ + return tr("Adds support for the game Fallout 3."); +} + +MOBase::VersionInfo GameFallout3::version() const +{ + return VersionInfo(1, 4, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameFallout3::settings() const +{ + return QList(); +} + +void GameFallout3::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Fallout3", path, "plugins.txt"); + copyToProfile(localAppFolder() + "/Fallout3", path, "loadorder.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath(), "fallout.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout_default.ini", + "fallout.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout.ini"); + } + + copyToProfile(myGamesPath(), path, "falloutprefs.ini"); + copyToProfile(myGamesPath(), path, "FalloutCustom.ini"); + copyToProfile(myGamesPath(), path, "custom.ini"); + copyToProfile(myGamesPath(), path, "GECKCustom.ini"); + copyToProfile(myGamesPath(), path, "GECKPrefs.ini"); + } +} + +QString GameFallout3::savegameExtension() const +{ + return "fos"; +} + +QString GameFallout3::savegameSEExtension() const +{ + return ""; +} + +std::shared_ptr +GameFallout3::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFallout3::steamAPPId() const +{ + if (selectedVariant() == "Steam (Game Of The Year)") { + return "22370"; + } else if (selectedVariant() == "Steam (Regular)") { + return "22300"; + } else { + return ""; + } +} + +QStringList GameFallout3::primaryPlugins() const +{ + return {"fallout3.esm"}; +} + +QStringList GameFallout3::gameVariants() const +{ + return {"Steam (Regular)", "Steam (Game Of The Year)", "Epic Games", "GOG", + "Low Violence"}; +} + +QString GameFallout3::binaryName() const +{ + if (selectedVariant() == "Low Violence") { + return "Fallout3ng.exe"; + } else { + return GameGamebryo::binaryName(); + } +} + +QString GameFallout3::gameShortName() const +{ + return "Fallout3"; +} + +QStringList GameFallout3::validShortNames() const +{ + return {"FalloutNV"}; +} + +QString GameFallout3::gameNexusName() const +{ + return "fallout3"; +} + +QStringList GameFallout3::iniFiles() const +{ + return {"fallout.ini", "falloutprefs.ini", "FalloutCustom.ini", "GECKCustom.ini", + "GECKPrefs.ini"}; +} + +QStringList GameFallout3::DLCPlugins() const +{ + return {"ThePitt.esm", "Anchorage.esm", "BrokenSteel.esm", "PointLookout.esm", + "Zeta.esm"}; +} + +int GameFallout3::nexusModOrganizerID() const +{ + return 16348; +} + +int GameFallout3::nexusGameID() const +{ + return 120; +} + +QString GameFallout3::getLauncherName() const +{ + const QMap names = { + {"Steam (Regular)", "Fallout3Launcher.exe"}, + {"Steam (Game of the Year)", "Fallout3Launcher.exe"}, + {"Epic Games", "FalloutLauncherEpic.exe"}, + {"GOG", "FalloutLauncher.exe"}, + {"Low Violence", "Fallout3Launcher.exe"}}; + + return names.value(selectedVariant()); +} diff --git a/libs/game_bethesda/src/games/fallout3/gamefallout3.h b/libs/game_bethesda/src/games/fallout3/gamefallout3.h new file mode 100644 index 0000000..a6dce27 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/gamefallout3.h @@ -0,0 +1,58 @@ +#ifndef GAMEFALLOUT3_H +#define GAMEFALLOUT3_H + +#include "gamegamebryo.h" + +#include +#include + +class GameFallout3 : public GameGamebryo +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.tannin.GameFallout3" FILE "gamefallout3.json") + +public: + GameFallout3(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual void detectGame() override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const; + QString binaryName() const override; + virtual QString gameShortName() const override; + virtual QStringList validShortNames() const override; + virtual QString gameNexusName() const override; + virtual QString getLauncherName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +protected: + virtual QString identifyGamePath() const override; + virtual QString savegameExtension() const override; + virtual QString savegameSEExtension() const override; + std::shared_ptr makeSaveGame(QString filePath) const override; + +private: + QString identifyVariant() const; +}; + +#endif // GAMEFALLOUT3_H diff --git a/libs/game_bethesda/src/games/fallout3/gamefallout3.json b/libs/game_bethesda/src/games/fallout3/gamefallout3.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/gamefallout3.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/fallout4/CMakeLists.txt b/libs/game_bethesda/src/games/fallout4/CMakeLists.txt new file mode 100644 index 0000000..da33980 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_fallout4 SHARED + fallout4bsainvalidation.cpp + fallout4bsainvalidation.h + fallout4dataarchives.cpp + fallout4dataarchives.h + fallout4moddatachecker.h + fallout4moddatacontent.h + fallout4savegame.cpp + fallout4savegame.h + fallout4scriptextender.cpp + fallout4scriptextender.h + fallout4unmanagedmods.cpp + fallout4unmanagedmods.h + gamefallout4.cpp + gamefallout4.h +) +mo2_configure_plugin(game_fallout4 NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_fallout4 PRIVATE game_creation) +mo2_install_plugin(game_fallout4) diff --git a/libs/game_bethesda/src/games/fallout4/fallout4.qrc b/libs/game_bethesda/src/games/fallout4/fallout4.qrc new file mode 100644 index 0000000..c8e5214 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4.qrc @@ -0,0 +1,5 @@ + + + splash.png + + diff --git a/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp b/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp new file mode 100644 index 0000000..7711522 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp @@ -0,0 +1,68 @@ +#include "fallout4bsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include +#include + +#include "gamegamebryo.h" + +Fallout4BSAInvalidation::Fallout4BSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "Fallout4Custom.ini", game) +{ + m_IniFileName = "Fallout4Custom.ini"; + m_Game = game; +} + +bool Fallout4BSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + return false; +} + +QString Fallout4BSAInvalidation::invalidationBSAName() const +{ + return ""; +} + +unsigned long Fallout4BSAInvalidation::bsaVersion() const +{ + return 0x68; +} + +bool Fallout4BSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + + if (profile->invalidationActive(nullptr)) { + // write bInvalidateOlderFiles = 1, if needed + QString bInvalidateOlderFiles = GameGamebryo::readIniValue( + iniFilePath, "Archive", "bInvalidateOlderFiles", "0"); + if (bInvalidateOlderFiles.toLong() != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "bInvalidateOlderFiles", "1", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + QString sResourceDataDirsFinal = GameGamebryo::readIniValue( + iniFilePath, "Archive", "sResourceDataDirsFinal", "STRINGS\\"); + if (sResourceDataDirsFinal != "") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "sResourceDataDirsFinal", "", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.h b/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.h new file mode 100644 index 0000000..fe52505 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.h @@ -0,0 +1,33 @@ +#ifndef FALLOUT4BSAINVALIDATION_H +#define FALLOUT4BSAINVALIDATION_H + +#include "fallout4dataarchives.h" +#include "gamebryobsainvalidation.h" +#include +#include + +#include + +namespace MOBase +{ +class IPluginGame; +} + +class Fallout4BSAInvalidation : public GamebryoBSAInvalidation +{ +public: + Fallout4BSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + virtual bool isInvalidationBSA(const QString& bsaName) override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; + +private: + QString m_IniFileName; + MOBase::IPluginGame const* m_Game; +}; + +#endif // FALLOUT4BSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp b/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp new file mode 100644 index 0000000..c0931ce --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp @@ -0,0 +1,48 @@ +#include "fallout4dataarchives.h" + +#include "iprofile.h" +#include + +QStringList Fallout4DataArchives::vanillaArchives() const +{ + return {"Fallout4 - Textures1.ba2", "Fallout4 - Textures2.ba2", + "Fallout4 - Textures3.ba2", "Fallout4 - Textures4.ba2", + "Fallout4 - Textures5.ba2", "Fallout4 - Textures6.ba2", + "Fallout4 - Textures7.ba2", "Fallout4 - Textures8.ba2", + "Fallout4 - Textures9.ba2", "Fallout4 - Meshes.ba2", + "Fallout4 - MeshesExtra.ba2", "Fallout4 - Voices.ba2", + "Fallout4 - Sounds.ba2", "Fallout4 - Interface.ba2", + "Fallout4 - Animations.ba2", "Fallout4 - Materials.ba2", + "Fallout4 - Shaders.ba2", "Fallout4 - Startup.ba2", + "Fallout4 - Misc.ba2"}; +} + +QStringList Fallout4DataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void Fallout4DataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.h b/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.h new file mode 100644 index 0000000..c90711c --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.h @@ -0,0 +1,27 @@ +#ifndef FALLOUT4DATAARCHIVES_H +#define FALLOUT4DATAARCHIVES_H + +#include "gamebryodataarchives.h" + +namespace MOBase +{ +class IProfile; +} + +#include +#include + +class Fallout4DataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // FALLOUT4DATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/fallout4/fallout4moddatachecker.h b/libs/game_bethesda/src/games/fallout4/fallout4moddatachecker.h new file mode 100644 index 0000000..08b0497 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4moddatachecker.h @@ -0,0 +1,29 @@ +#ifndef FALLOUT4_MODATACHECKER_H +#define FALLOUT4_MODATACHECKER_H + +#include + +class Fallout4ModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + "interface", "meshes", "music", "scripts", "sound", "strings", + "textures", "trees", "video", "materials", "f4se", "distantlod", + "asi", "Tools", "MCM", "distantland", "mits", "dllplugins", + "CalienteTools", "shadersfx", "aaf"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "esl", "ba2", + "modgroups", "ini", "csg", "cdx"}; + return result; + } +}; + +#endif // FALLOUT4_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/fallout4/fallout4moddatacontent.h b/libs/game_bethesda/src/games/fallout4/fallout4moddatacontent.h new file mode 100644 index 0000000..172e94f --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4moddatacontent.h @@ -0,0 +1,44 @@ +#ifndef FALLOUT4_MODDATACONTENT_H +#define FALLOUT4_MODDATACONTENT_H + +#include +#include + +class Fallout4ModDataContent : public GamebryoModDataContent +{ +protected: + enum Fallout4Content + { + CONTENT_MATERIAL = CONTENT_NEXT_VALUE + }; + +public: + Fallout4ModDataContent(const MOBase::IGameFeatures* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + m_Enabled[CONTENT_SKYPROC] = false; + } + + std::vector getAllContents() const override + { + auto contents = GamebryoModDataContent::getAllContents(); + contents.push_back( + Content(CONTENT_MATERIAL, "Materials", ":/MO/gui/content/material")); + return contents; + } + + std::vector + getContentsFor(std::shared_ptr fileTree) const override + { + auto contents = GamebryoModDataContent::getContentsFor(fileTree); + for (auto e : *fileTree) { + if (e->compare("materials") == 0) { + contents.push_back(CONTENT_MATERIAL); + break; // Early break if you have nothing else to check. + } + } + return contents; + } +}; + +#endif // FALLOUT4_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp b/libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp new file mode 100644 index 0000000..2f88d1b --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp @@ -0,0 +1,106 @@ +#include "fallout4savegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gamefallout4.h" + +Fallout4SaveGame::Fallout4SaveGame(QString const& fileName, GameFallout4 const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); + + FILETIME creationTime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, + creationTime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&creationTime, &ctime); + + setCreationTime(ctime); +} + +void Fallout4SaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const +{ + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); +} + +std::unique_ptr Fallout4SaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); // 10bytes + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + QString ignore; + std::unique_ptr fields = std::make_unique(); + + fields->Screenshot = file.readImage(384, true); + + uint8_t saveGameVersion = file.readChar(); + file.read(ignore); // game version + file.skip(); // plugin info size + + fields->Plugins = file.readPlugins(); + if (saveGameVersion >= 68) { + fields->LightPlugins = file.readLightPlugins(); + } + + return fields; +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4savegame.h b/libs/game_bethesda/src/games/fallout4/fallout4savegame.h new file mode 100644 index 0000000..b50c501 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4savegame.h @@ -0,0 +1,23 @@ +#ifndef FALLOUT4SAVEGAME_H +#define FALLOUT4SAVEGAME_H + +#include "gamebryosavegame.h" +#include "windows_compat.h" + +class GameFallout4; + +class Fallout4SaveGame : public GamebryoSaveGame +{ +public: + Fallout4SaveGame(QString const& fileName, GameFallout4 const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& file, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // FALLOUT4SAVEGAME_H diff --git a/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp b/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp new file mode 100644 index 0000000..1207d5e --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp @@ -0,0 +1,18 @@ +#include "fallout4scriptextender.h" + +#include +#include + +Fallout4ScriptExtender::Fallout4ScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString Fallout4ScriptExtender::BinaryName() const +{ + return "f4se_loader.exe"; +} + +QString Fallout4ScriptExtender::PluginPath() const +{ + return "f4se/plugins"; +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.h b/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.h new file mode 100644 index 0000000..b29df05 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.h @@ -0,0 +1,17 @@ +#ifndef FALLOUT4SCRIPTEXTENDER_H +#define FALLOUT4SCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class Fallout4ScriptExtender : public GamebryoScriptExtender +{ +public: + Fallout4ScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // FALLOUT4SCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp b/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp new file mode 100644 index 0000000..61b7b2a --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp @@ -0,0 +1,63 @@ +#include "fallout4unmanagedmods.h" + +Fallout4UnmangedMods::Fallout4UnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +Fallout4UnmangedMods::~Fallout4UnmangedMods() {} + +QStringList Fallout4UnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} + +QStringList Fallout4UnmangedMods::secondaryFiles(const QString& modName) const +{ + // file extension in FO4 is .ba2 instead of bsa + QStringList archives; + QDir dataDir = game()->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.ba2"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} + +QString Fallout4UnmangedMods::displayName(const QString& modName) const +{ + // unlike in earlier games, in fallout 4 the file name doesn't correspond to + // the public name + if (modName.compare("dlcrobot", Qt::CaseInsensitive) == 0) { + return "Automatron"; + } else if (modName.compare("dlcworkshop01", Qt::CaseInsensitive) == 0) { + return "Wasteland Workshop"; + } else if (modName.compare("dlccoast", Qt::CaseInsensitive) == 0) { + return "Far Harbor"; + } else if (modName.compare("dlcworkshop02", Qt::CaseInsensitive) == 0) { + return "Contraptions Workshop"; + } else if (modName.compare("dlcworkshop03", Qt::CaseInsensitive) == 0) { + return "Vault-Tec Workshop"; + } else if (modName.compare("dlcnukaworld", Qt::CaseInsensitive) == 0) { + return "Nuka-World"; + } else if (modName.compare("dlcultrahighresolution", Qt::CaseInsensitive) == 0) { + return "Ultra High Resolution Texture Pack"; + } else { + return modName; + } +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.h b/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.h new file mode 100644 index 0000000..6078b73 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.h @@ -0,0 +1,18 @@ +#ifndef FALLOUT4UNMANAGEDMODS_H +#define FALLOUT4UNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include + +class Fallout4UnmangedMods : public GamebryoUnmangedMods +{ +public: + Fallout4UnmangedMods(const GameGamebryo* game); + ~Fallout4UnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; + virtual QStringList secondaryFiles(const QString& modName) const override; + virtual QString displayName(const QString& modName) const override; +}; + +#endif // FALLOUT4UNMANAGEDMODS_H diff --git a/libs/game_bethesda/src/games/fallout4/game_fallout4_en.ts b/libs/game_bethesda/src/games/fallout4/game_fallout4_en.ts new file mode 100644 index 0000000..1c57784 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/game_fallout4_en.ts @@ -0,0 +1,28 @@ + + + + + GameFallout4 + + + Fallout 4 Support Plugin + + + + + Adds support for the game Fallout 4. +Splash by %1 + + + + + sTestFile entries are present + + + + + <p>You have sTestFile settings in your Fallout4Custom.ini. These must be removed or the game will not read the plugins.txt file. Management is disabled.</p> + + + + diff --git a/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp b/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp new file mode 100644 index 0000000..8939a3c --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp @@ -0,0 +1,334 @@ +#include "gamefallout4.h" + +#include "fallout4bsainvalidation.h" +#include "fallout4dataarchives.h" +#include "fallout4moddatachecker.h" +#include "fallout4moddatacontent.h" +#include "fallout4savegame.h" +#include "fallout4scriptextender.h" +#include "fallout4unmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "scopeguard.h" + +using namespace MOBase; + +const unsigned int GameFallout4::PROBLEM_TEST_FILE; + +GameFallout4::GameFallout4() {} + +bool GameFallout4::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(this, "fallout4custom.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(dataArchives.get(), this)); + + return true; +} + +QString GameFallout4::gameName() const +{ + return "Fallout 4"; +} + +void GameFallout4::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath("Fallout4"); +} + +QList GameFallout4::executables() const +{ + return QList() + << ExecutableInfo("F4SE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Fallout 4", findInGameFolder(binaryName())) + << ExecutableInfo("Fallout Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("1946160") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Fallout4\""); +} + +QList GameFallout4::executableForcedLoads() const +{ + return QList(); +} + +QString GameFallout4::name() const +{ + return "Fallout 4 Support Plugin"; +} + +QString GameFallout4::localizedName() const +{ + return tr("Fallout 4 Support Plugin"); +} + +QString GameFallout4::author() const +{ + return "Tannin & MO2 Team"; +} + +QString GameFallout4::description() const +{ + return tr("Adds support for the game Fallout 4.\n" + "Splash by %1") + .arg("nekoyoubi"); +} + +MOBase::VersionInfo GameFallout4::version() const +{ + return VersionInfo(1, 8, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameFallout4::settings() const +{ + return QList(); +} + +MappingType GameFallout4::mappings() const +{ + MappingType result; + if (testFilePlugins().isEmpty()) { + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameShortName() + "/" + profileFile, + false}); + } + } + return result; +} + +void GameFallout4::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Fallout4", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/fallout4.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout4_default.ini", + "fallout4.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout4.ini"); + } + + copyToProfile(myGamesPath(), path, "fallout4prefs.ini"); + copyToProfile(myGamesPath(), path, "fallout4custom.ini"); + } +} + +QString GameFallout4::savegameExtension() const +{ + return "fos"; +} + +QString GameFallout4::savegameSEExtension() const +{ + return "f4se"; +} + +std::shared_ptr +GameFallout4::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFallout4::steamAPPId() const +{ + return "377160"; +} + +QStringList GameFallout4::testFilePlugins() const +{ + QStringList plugins; + if (m_Organizer != nullptr && m_Organizer->profile() != nullptr) { + QString customIni( + m_Organizer->profile()->absoluteIniFilePath("Fallout4Custom.ini")); + if (QFile(customIni).exists()) { + for (int i = 1; i <= 10; ++i) { + QString setting("sTestFile"); + setting += std::to_string(i); + QString plugin = GameGamebryo::readIniValue(customIni, "General", setting, ""); + if (!plugin.isEmpty() && !plugins.contains(plugin, Qt::CaseInsensitive)) { + plugins.append(plugin); + } + } + } + } + return plugins; +} + +QStringList GameFallout4::primaryPlugins() const +{ + QStringList plugins = {"fallout4.esm", "dlcrobot.esm", + "dlcworkshop01.esm", "dlccoast.esm", + "dlcworkshop02.esm", "dlcworkshop03.esm", + "dlcnukaworld.esm", "dlcultrahighresolution.esm"}; + + auto testPlugins = testFilePlugins(); + if (loadOrderMechanism() == LoadOrderMechanism::None) { + plugins << testPlugins; + } else { + plugins << CCPlugins(); + } + + return plugins; +} + +QStringList GameFallout4::gameVariants() const +{ + return {"Regular"}; +} + +QString GameFallout4::gameShortName() const +{ + return "Fallout4"; +} + +QString GameFallout4::gameNexusName() const +{ + return "fallout4"; +} + +QStringList GameFallout4::iniFiles() const +{ + return {"fallout4.ini", "fallout4prefs.ini", "fallout4custom.ini"}; +} + +QStringList GameFallout4::DLCPlugins() const +{ + return {"dlcrobot.esm", + "dlcworkshop01.esm", + "dlccoast.esm", + "dlcworkshop02.esm", + "dlcworkshop03.esm", + "dlcnukaworld.esm", + "dlcultrahighresolution.esm"}; +} + +QStringList GameFallout4::CCPlugins() const +{ + QStringList plugins = {}; + QFile file(gameDirectory().absoluteFilePath("Fallout4.ccc")); + if (file.open(QIODevice::ReadOnly)) { + ON_BLOCK_EXIT([&file]() { + file.close(); + }); + + if (file.size() == 0) { + return plugins; + } + while (!file.atEnd()) { + QByteArray line = file.readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()).toLower(); + } + + if (modName.size() > 0) { + if (!plugins.contains(modName, Qt::CaseInsensitive)) { + plugins.append(modName); + } + } + } + } + return plugins; +} + +IPluginGame::SortMechanism GameFallout4::sortMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::SortMechanism::LOOT; + return IPluginGame::SortMechanism::NONE; +} + +IPluginGame::LoadOrderMechanism GameFallout4::loadOrderMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::LoadOrderMechanism::PluginsTxt; + return IPluginGame::LoadOrderMechanism::None; +} + +int GameFallout4::nexusModOrganizerID() const +{ + return 28715; +} + +int GameFallout4::nexusGameID() const +{ + return 1151; +} + +// Start Diagnose +std::vector GameFallout4::activeProblems() const +{ + std::vector result; + if (m_Organizer->managedGame() == this) { + if (testFilePresent()) + result.push_back(PROBLEM_TEST_FILE); + } + return result; +} + +bool GameFallout4::testFilePresent() const +{ + if (!testFilePlugins().isEmpty()) + return true; + return false; +} + +QString GameFallout4::shortDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_TEST_FILE: + return tr("sTestFile entries are present"); + } + return QString(); +} + +QString GameFallout4::fullDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_TEST_FILE: { + return tr("

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

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

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

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

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

" + "

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

" + "

Notably, xEdit does not currently support saving ESP files.

" + "

Current ESPs:

%1

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

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

"); + } + } + return ""; +} + +bool GameStarfield::hasGuidedFix(unsigned int key) const +{ + return false; +} + +void GameStarfield::startGuidedFix(unsigned int key) const {} diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.h b/libs/game_bethesda/src/games/starfield/gamestarfield.h new file mode 100644 index 0000000..f9724c0 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/gamestarfield.h @@ -0,0 +1,83 @@ +#ifndef GAMESTARFIELD_H +#define GAMESTARFIELD_H + +#include "gamegamebryo.h" +#include "iplugindiagnose.h" + +#include +#include + +class GameStarfield : public GameGamebryo, public MOBase::IPluginDiagnose +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame MOBase::IPluginDiagnose) + Q_PLUGIN_METADATA(IID "org.modorganizer.GameStarfield" FILE "gamestarfield.json") + +public: + GameStarfield(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: + QStringList testFilePlugins() const; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual void detectGame() override; + virtual QDir dataDirectory() const override; + virtual QMap secondaryDataDirectories() const override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList enabledPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString gameShortName() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual bool prepareIni(const QString& exec) override; + virtual QStringList DLCPlugins() const override; + virtual QStringList CCPlugins() const override; + virtual SortMechanism sortMechanism() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + +public: // IPluginDiagnose interface + virtual std::vector activeProblems() const override; + virtual QString shortDescription(unsigned int key) const override; + virtual QString fullDescription(unsigned int key) const override; + virtual bool hasGuidedFix(unsigned int key) const override; + virtual void startGuidedFix(unsigned int key) const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + virtual MappingType mappings() const override; + +protected: + virtual QString identifyGamePath() const override; + std::shared_ptr makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; + +private: + QStringList CCCPlugins() const; + bool activeESP() const; + bool testFilePresent() const; + +private: + static const unsigned int PROBLEM_ESP = 1; + static const unsigned int PROBLEM_TEST_FILE = 2; + + mutable std::set m_Active_ESPs; +}; + +#endif // GAMEStarfield_H diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.json b/libs/game_bethesda/src/games/starfield/gamestarfield.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/gamestarfield.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp new file mode 100644 index 0000000..f6e803b --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp @@ -0,0 +1,68 @@ +#include "starfieldbsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include +#include + +#include "gamegamebryo.h" + +StarfieldBSAInvalidation::StarfieldBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "StarfieldCustom.ini", game) +{ + m_IniFileName = "StarfieldCustom.ini"; + m_Game = game; +} + +bool StarfieldBSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + return false; +} + +QString StarfieldBSAInvalidation::invalidationBSAName() const +{ + return ""; +} + +unsigned long StarfieldBSAInvalidation::bsaVersion() const +{ + return 0x68; +} + +bool StarfieldBSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + + if (profile->invalidationActive(nullptr)) { + // write bInvalidateOlderFiles = 1, if needed + QString bInvalidateOlderFiles = GameGamebryo::readIniValue( + iniFilePath, "Archive", "bInvalidateOlderFiles", "0"); + if (bInvalidateOlderFiles.toLong() != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "bInvalidateOlderFiles", "1", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + QString sResourceDataDirsFinal = GameGamebryo::readIniValue( + iniFilePath, "Archive", "sResourceDataDirsFinal", "STRINGS\\"); + if (sResourceDataDirsFinal != "") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "sResourceDataDirsFinal", "", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.h b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.h new file mode 100644 index 0000000..40ec355 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.h @@ -0,0 +1,33 @@ +#ifndef STARFIELDBSAINVALIDATION_H +#define STARFIELDBSAINVALIDATION_H + +#include "gamebryobsainvalidation.h" +#include "starfielddataarchives.h" +#include +#include + +#include + +namespace MOBase +{ +class IPluginGame; +} + +class StarfieldBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + StarfieldBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + virtual bool isInvalidationBSA(const QString& bsaName) override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; + +private: + QString m_IniFileName; + MOBase::IPluginGame const* m_Game; +}; + +#endif // STARFIELDBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp b/libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp new file mode 100644 index 0000000..d7eadae --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp @@ -0,0 +1,98 @@ +#include "starfielddataarchives.h" + +#include "iprofile.h" +#include + +QStringList StarfieldDataArchives::vanillaArchives() const +{ + return {"Starfield - Animations.ba2", + "Starfield - DensityMaps.ba2", + "Starfield - FaceAnimation01.ba2", + "Starfield - FaceAnimation02.ba2", + "Starfield - FaceAnimation03.ba2", + "Starfield - FaceAnimation04.ba2", + "Starfield - FaceAnimationPatch.ba2", + "Starfield - FaceMeshes.ba2", + "Starfield - GeneratedTextures.ba2", + "Starfield - Interface.ba2", + "Starfield - Localization.ba2", + "Starfield - LODMeshes.ba2", + "Starfield - LODMeshesPatch.ba2", + "Starfield - LODTextures.ba2", + "Starfield - Materials.ba2", + "Starfield - Meshes01.ba2", + "Starfield - Meshes02.ba2", + "Starfield - MeshesPatch.ba2", + "Starfield - Misc.ba2", + "Starfield - Particles.ba2", + "Starfield - ParticlesTestData.ba2", + "Starfield - PlanetData.ba2", + "Starfield - Shaders.ba2", + "Starfield - ShadersBeta.ba2", + "Starfield - Terrain01.ba2", + "Starfield - Terrain02.ba2", + "Starfield - Terrain03.ba2", + "Starfield - Terrain04.ba2", + "Starfield - TerrainPatch.ba2", + "Starfield - Textures01.ba2", + "Starfield - Textures02.ba2", + "Starfield - Textures03.ba2", + "Starfield - Textures04.ba2", + "Starfield - Textures05.ba2", + "Starfield - Textures06.ba2", + "Starfield - Textures07.ba2", + "Starfield - Textures08.ba2", + "Starfield - Textures09.ba2", + "Starfield - Textures10.ba2", + "Starfield - Textures11.ba2", + "Starfield - TexturesPatch.ba2", + "Starfield - Voices01.ba2", + "Starfield - Voices02.ba2", + "Starfield - VoicesPatch.ba2", + "Starfield - WwiseSounds01.ba2", + "Starfield - WwiseSounds02.ba2", + "Starfield - WwiseSounds03.ba2", + "Starfield - WwiseSounds04.ba2", + "Starfield - WwiseSounds05.ba2", + "Starfield - WwiseSoundsPatch.ba2", + "Constellation - Localization.ba2", + "Constellation - Textures.ba2", + "OldMars - Localization.ba2", + "OldMars - Textures.ba2", + "BlueprintShips-Starfield - Localization.ba2"}; +} + +QStringList StarfieldDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString defaultIniFile = gameDirectory().absoluteFilePath("Starfield.ini"); + QString customIniFile = + profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("StarfieldCustom.ini") + : localGameDirectory().absoluteFilePath("StarfieldCustom.ini"); + QStringList archiveSettings = {"SResourceArchiveList", "sResourceIndexFileList", + "SResourceArchiveMemoryCacheList", + "sResourceStartUpArchiveList", + "sResourceEnglishVoiceList"}; + for (auto setting : archiveSettings) { + auto archives = getArchivesFromKey(customIniFile, setting, 4096); + if (archives.isEmpty()) + archives = getArchivesFromKey(defaultIniFile, setting, 4096); + result.append(archives); + } + + return result; +} + +void StarfieldDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{} + +void StarfieldDataArchives::addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) +{} + +void StarfieldDataArchives::removeArchive(MOBase::IProfile* profile, + const QString& archiveName) +{} diff --git a/libs/game_bethesda/src/games/starfield/starfielddataarchives.h b/libs/game_bethesda/src/games/starfield/starfielddataarchives.h new file mode 100644 index 0000000..c83943f --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfielddataarchives.h @@ -0,0 +1,28 @@ +#ifndef STARFIELDDATAARCHIVES_H +#define STARFIELDDATAARCHIVES_H + +#include +#include + +#include "gamebryodataarchives.h" + +class GameGamebryo; + +class StarfieldDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + virtual void addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) override; + virtual void removeArchive(MOBase::IProfile* profile, + const QString& archiveName) override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // STARFIELDDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp new file mode 100644 index 0000000..6b4e6ab --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp @@ -0,0 +1,35 @@ +#include "starfieldgameplugins.h" + +using namespace MOBase; + +StarfieldGamePlugins::StarfieldGamePlugins(MOBase::IOrganizer* organizer) + : CreationGamePlugins(organizer) +{} + +bool StarfieldGamePlugins::mediumPluginsAreSupported() +{ + return true; +} + +bool StarfieldGamePlugins::blueprintPluginsAreSupported() +{ + return true; +} + +void StarfieldGamePlugins::writePluginList(const IPluginList* pluginList, + const QString& filePath) +{ + if (m_Organizer->managedGame()->loadOrderMechanism() == + MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) { + CreationGamePlugins::writePluginList(pluginList, filePath); + } +} + +QStringList StarfieldGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + if (m_Organizer->managedGame()->loadOrderMechanism() == + MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) { + return CreationGamePlugins::readPluginList(pluginList); + } + return {}; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldgameplugins.h b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.h new file mode 100644 index 0000000..a957b13 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.h @@ -0,0 +1,23 @@ +#ifndef _STARFIELDGAMEPLUGINS_H +#define _STARFIELDGAMEPLUGINS_H + +#include + +#include +#include + +class StarfieldGamePlugins : public CreationGamePlugins +{ + +public: + StarfieldGamePlugins(MOBase::IOrganizer* organizer); + +protected: + virtual bool mediumPluginsAreSupported() override; + virtual bool blueprintPluginsAreSupported() override; + virtual void writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) override; + virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override; +}; + +#endif // _STARFIELDGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldmoddatachecker.h b/libs/game_bethesda/src/games/starfield/starfieldmoddatachecker.h new file mode 100644 index 0000000..13c1fcc --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldmoddatachecker.h @@ -0,0 +1,29 @@ +#ifndef STARFIELD_MODATACHECKER_H +#define STARFIELD_MODATACHECKER_H + +#include + +class StarfieldModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + "interface", "meshes", "geometries", "music", "scripts", "sound", + "strings", "textures", "trees", "video", "materials", "sfse", + "distantlod", "asi", "Tools", "MCM", "distantland", "mits", + "dllplugins", "CalienteTools", "shadersfx", "aaf"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "esl", "ba2", + "modgroups", "ini", "csg", "cdx"}; + return result; + } +}; + +#endif // STARFIELD_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldmoddatacontent.h b/libs/game_bethesda/src/games/starfield/starfieldmoddatacontent.h new file mode 100644 index 0000000..d4b8014 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldmoddatacontent.h @@ -0,0 +1,55 @@ +#ifndef STARFIELD_MODDATACONTENT_H +#define STARFIELD_MODDATACONTENT_H + +#include +#include + +class StarfieldModDataContent : public GamebryoModDataContent +{ +protected: + enum StarfieldContent + { + CONTENT_MATERIAL = CONTENT_NEXT_VALUE, + CONTENT_GEOMETRIES = CONTENT_NEXT_VALUE + 1, + CONTENT_VIDEO = CONTENT_NEXT_VALUE + 2 + }; + +public: + StarfieldModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + m_Enabled[CONTENT_SKYPROC] = false; + } + + std::vector getAllContents() const override + { + static std::vector STARFIELD_CONTENTS{ + {CONTENT_MATERIAL, QT_TR_NOOP("Materials"), ":/MO/gui/content/material"}, + {CONTENT_GEOMETRIES, QT_TR_NOOP("Geometries"), ":/MO/gui/content/geometries"}, + {CONTENT_VIDEO, QT_TR_NOOP("Video"), ":/MO/gui/content/media"}}; + auto contents = GamebryoModDataContent::getAllContents(); + std::copy(std::begin(STARFIELD_CONTENTS), std::end(STARFIELD_CONTENTS), + std::back_inserter(contents)); + return contents; + } + + std::vector + getContentsFor(std::shared_ptr fileTree) const override + { + auto contents = GamebryoModDataContent::getContentsFor(fileTree); + for (auto e : *fileTree) { + if (e->isDir()) { + if (e->compare("materials") == 0) { + contents.push_back(CONTENT_MATERIAL); + } else if (e->compare("geometries") == 0) { + contents.push_back(CONTENT_GEOMETRIES); + } else if (e->compare("video") == 0) { + contents.push_back(CONTENT_VIDEO); + } + } + } + return contents; + } +}; + +#endif // STARFIELD_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp b/libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp new file mode 100644 index 0000000..49653cb --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp @@ -0,0 +1,157 @@ +#include "starfieldsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gamestarfield.h" + +StarfieldSaveGame::StarfieldSaveGame(QString const& fileName, GameStarfield const* game) + : GamebryoSaveGame(fileName, game, true, true) +{ + FileWrapper file(getFilepath(), "BCPS"); + + getData(file); + FILETIME creationTime; + unsigned char saveVersion; + fetchInformationFields(file, m_SaveNumber, saveVersion, m_PCName, m_PCLevel, + m_PCLocation, creationTime); + file.closeCompressedData(); + file.close(); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&creationTime, &ctime); + + setCreationTime(ctime); +} + +void StarfieldSaveGame::getData(FileWrapper& file) const +{ + file.skip(); // header version + file.skip(); // chunk compressed size array start location + file.skip(); // unknown (0?) + file.setCompressionType(1); + /* + * Parse following variables then begin decompressing data + * - 64-bit int = compressed data start location + * - 64-bit int = complete uncompressed data size + */ + file.openCompressedData(); + /* + * Remaining headers before start of compressed data: + * - 32-bit float (version? appears to be 2.0) + * - 64-bit int - size of uncompressed chunks (250 KiB) + * - 64-bit int - size of byte rows? (16 bytes) used to determine start of each + * compressed chunk + * - 32-bit int - number of chunks? + * - 'ZIP ' - denotes start of chunk compressed size array + * - compressed size array - array of 32-bit ints containing the compressed size of + * each compressed chunk (see number of chunks above) + */ +} + +void StarfieldSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, unsigned char& saveVersion, + QString& playerName, unsigned short& playerLevel, QString& playerLocation, + FILETIME& creationTime) const +{ + char fileID[12]; // SFS_SAVEGAME + unsigned int headerSize; + unsigned int version; + // file.read(fileID, 12); + headerSize = file.readInt(12); + version = file.readInt(); + saveVersion = file.readChar(); + saveNumber = file.readInt(); + file.read(playerName); + + unsigned int temp; + temp = file.readInt(); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + unsigned short gender; + gender = file.readShort(); // Player gender (0 = male) + float experience, experienceRequired; + experience = file.readFloat(); + experienceRequired = file.readFloat(); + + unsigned long long time = file.readLong(); + creationTime.dwLowDateTime = (DWORD)time; + creationTime.dwHighDateTime = time >> 32; +} + +std::unique_ptr StarfieldSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "BCPS"); // 10bytes + + getData(file); + FILETIME creationTime; + unsigned char saveVersion; + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, saveVersion, dummyName, dummyLevel, + dummyLocation, dummyTime); + } + + int extraInfo = 0; + if (saveVersion >= 122) + extraInfo = 1; + if (saveVersion >= 140) + extraInfo = 2; + QStringList gamePlugins = m_Game->primaryPlugins() + m_Game->enabledPlugins(); + + QString ignore; + std::unique_ptr fields = std::make_unique(); + + file.readChar(12); + file.read(ignore); // game version + file.read(ignore); // game version again? + file.readInt(); // plugin info size + + fields->Plugins = file.readPlugins(0, extraInfo, gamePlugins); + fields->LightPlugins = file.readLightPlugins(0, extraInfo, gamePlugins); + if (saveVersion >= 122) + fields->MediumPlugins = file.readMediumPlugins(0, extraInfo, gamePlugins); + file.closeCompressedData(); + file.close(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldsavegame.h b/libs/game_bethesda/src/games/starfield/starfieldsavegame.h new file mode 100644 index 0000000..7bd07e0 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldsavegame.h @@ -0,0 +1,27 @@ +#ifndef STARFIELDSAVEGAME_H +#define STARFIELDSAVEGAME_H + +#include "gamebryosavegame.h" +#include "windows_compat.h" +#include + +class GameStarfield; + +class StarfieldSaveGame : public GamebryoSaveGame +{ +public: + StarfieldSaveGame(QString const& fileName, GameStarfield const* game); + +protected: + // Fetch easy-to-access information. + void getData(FileWrapper& file) const; + + void fetchInformationFields(FileWrapper& file, uint32_t& saveNumber, + unsigned char& saveVersion, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, + FILETIME& creationTime) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // STARFIELDSAVEGAME_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp new file mode 100644 index 0000000..15cf6cd --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp @@ -0,0 +1,18 @@ +#include "starfieldscriptextender.h" + +#include +#include + +StarfieldScriptExtender::StarfieldScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString StarfieldScriptExtender::BinaryName() const +{ + return "sfse_loader.exe"; +} + +QString StarfieldScriptExtender::PluginPath() const +{ + return "sfse/plugins"; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldscriptextender.h b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.h new file mode 100644 index 0000000..b7c27f1 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.h @@ -0,0 +1,17 @@ +#ifndef STARFIELDSCRIPTEXTENDER_H +#define STARFIELDSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class StarfieldScriptExtender : public GamebryoScriptExtender +{ +public: + StarfieldScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // STARFIELDSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp new file mode 100644 index 0000000..30abb38 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp @@ -0,0 +1,132 @@ +#include "starfieldunmanagedmods.h" + +#include "log.h" + +#include +#include +#include +#include + +StarfieldUnmanagedMods::StarfieldUnmanagedMods(const GameStarfield* game, + const QString& appDataFolder) + : GamebryoUnmangedMods(game), m_AppDataFolder(appDataFolder) +{} + +StarfieldUnmanagedMods::~StarfieldUnmanagedMods() {} + +QStringList StarfieldUnmanagedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins() + game()->enabledPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QMap directories = {{"data", game()->dataDirectory()}}; + directories.insert(game()->secondaryDataDirectories()); + for (QDir directory : directories) { + for (const QString& fileName : directory.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + } + + return result; +} + +QFileInfo StarfieldUnmanagedMods::referenceFile(const QString& modName) const +{ + QFileInfoList files; + QMap directories = {{"data", game()->dataDirectory()}}; + directories.insert(game()->secondaryDataDirectories()); + for (QDir directory : directories) { + files += directory.entryInfoList(QStringList() << modName + ".es*"); + } + if (files.size() > 0) { + return files.at(0); + } else { + return QFileInfo(); + } +} + +std::map +StarfieldUnmanagedMods::parseContentCatalog() const +{ + QFile content(m_AppDataFolder + "/" + game()->gameShortName() + + "/ContentCatalog.txt"); + std::map contentCatalog; + if (content.open(QIODevice::OpenModeFlag::ReadOnly)) { + auto contentData = content.readAll(); + QString convertedData = QString::fromLatin1(contentData); + contentData = convertedData.toUtf8(); + QJsonParseError jsonError; + QJsonDocument contentDoc = QJsonDocument::fromJson(contentData, &jsonError); + if (jsonError.error) { + MOBase::log::warn(QObject::tr("ContentCatalog.txt appears to be corrupt: %1") + .arg(jsonError.errorString())); + } else { + QJsonObject contentObj = contentDoc.object(); + for (const auto& mod : contentObj.keys()) { + if (mod == "ContentCatalog") + continue; + auto modInfo = contentObj.value(mod).toObject(); + QStringList pluginList; + QStringList files; + for (const auto& file : modInfo.value("Files").toArray()) { + QString fileName = file.toString(); + files.append(fileName); + if (fileName.endsWith(".esm", Qt::CaseInsensitive) || + fileName.endsWith(".esl", Qt::CaseInsensitive) || + fileName.endsWith(".esp", Qt::CaseInsensitive)) { + pluginList.append(fileName); + } + } + for (const auto& plugin : pluginList) { + contentCatalog[plugin] = ContentCatalog(); + contentCatalog[plugin].files = files; + contentCatalog[plugin].name = modInfo.value("Title").toString(); + } + } + } + } + return contentCatalog; +} + +QStringList StarfieldUnmanagedMods::secondaryFiles(const QString& modName) const +{ + QStringList files; + auto contentCatalog = parseContentCatalog(); + for (const auto& mod : contentCatalog) { + if (mod.first.startsWith(modName, Qt::CaseInsensitive)) { + files += mod.second.files; + break; + } + } + // file extension in FO4 is .ba2 instead of bsa + QMap directories = {{"data", game()->dataDirectory()}}; + directories.insert(game()->secondaryDataDirectories()); + for (QDir directory : directories) { + for (const QString& archiveName : directory.entryList({modName + "*.ba2"})) { + files.append(directory.absoluteFilePath(archiveName)); + } + } + return files; +} + +QString StarfieldUnmanagedMods::displayName(const QString& modName) const +{ + auto contentCatalog = parseContentCatalog(); + for (const auto& mod : contentCatalog) { + if (mod.first.startsWith(modName, Qt::CaseInsensitive)) { + return mod.second.name; + } + } + // unlike in earlier games, in fallout 4 the file name doesn't correspond to + // the public name + return modName; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h new file mode 100644 index 0000000..abe14ef --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h @@ -0,0 +1,36 @@ +#ifndef STARFIELDUNMANAGEDMODS_H +#define STARFIELDUNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include "gamestarfield.h" +#include + +#include + +class StarfieldUnmanagedMods : public GamebryoUnmangedMods +{ + friend class GameStarfield; + + struct ContentCatalog + { + QString name; + QStringList files; + }; + +public: + StarfieldUnmanagedMods(const GameStarfield* game, const QString& appDataFolder); + ~StarfieldUnmanagedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; + virtual QFileInfo referenceFile(const QString& modName) const override; + virtual QStringList secondaryFiles(const QString& modName) const override; + virtual QString displayName(const QString& modName) const override; + +private: + std::map parseContentCatalog() const; + +private: + QString m_AppDataFolder; +}; + +#endif // STARFIELDUNMANAGEDMODS_H diff --git a/libs/game_bethesda/src/games/ttw/CMakeLists.txt b/libs/game_bethesda/src/games/ttw/CMakeLists.txt new file mode 100644 index 0000000..27149ec --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_ttw SHARED + falloutttwbsainvalidation.cpp + falloutttwbsainvalidation.h + falloutttwdataarchives.cpp + falloutttwdataarchives.h + falloutttwmoddatachecker.h + falloutttwmoddatacontent.h + falloutttwsavegame.cpp + falloutttwsavegame.h + falloutttwscriptextender.cpp + falloutttwscriptextender.h + gamefalloutttw.cpp + gamefalloutttw.h +) +mo2_configure_plugin(game_ttw NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_ttw PRIVATE game_gamebryo) +mo2_install_plugin(game_ttw) diff --git a/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp b/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp new file mode 100644 index 0000000..67a7817 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "falloutttwbsainvalidation.h" + +FalloutTTWBSAInvalidation::FalloutTTWBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "fallout.ini", game) +{} + +QString FalloutTTWBSAInvalidation::invalidationBSAName() const +{ + return "Fallout - Invalidation.bsa"; +} + +unsigned long FalloutTTWBSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.h b/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.h new file mode 100644 index 0000000..96062d3 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.h @@ -0,0 +1,20 @@ +#ifndef FALLOUTTTWBSAINVALIDATION_H +#define FALLOUTTTWBSAINVALIDATION_H + +#include "falloutttwdataarchives.h" +#include "gamebryobsainvalidation.h" + +#include + +class FalloutTTWBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + FalloutTTWBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; +}; + +#endif // FALLOUTTTWBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp b/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp new file mode 100644 index 0000000..3ffb076 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp @@ -0,0 +1,31 @@ +#include "falloutttwdataarchives.h" +#include + +QStringList FalloutTTWDataArchives::vanillaArchives() const +{ + return {"Fallout - Textures.bsa", "Fallout - Textures2.bsa", "Fallout - Meshes.bsa", + "Fallout - Voices1.bsa", "Fallout - Sound.bsa", "Fallout - Misc.bsa"}; +} + +QStringList FalloutTTWDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + result.append(getArchivesFromKey(iniFile, "SArchiveList")); + + return result; +} + +void FalloutTTWDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + setArchivesToKey(iniFile, "SArchiveList", list); +} diff --git a/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.h b/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.h new file mode 100644 index 0000000..eb9d7c7 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.h @@ -0,0 +1,23 @@ +#ifndef FALLOUTTTWDATAARCHIVES_H +#define FALLOUTTTWDATAARCHIVES_H + +#include +#include +#include +#include +#include + +class FalloutTTWDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // FALLOUTTTWDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/ttw/falloutttwmoddatachecker.h b/libs/game_bethesda/src/games/ttw/falloutttwmoddatachecker.h new file mode 100644 index 0000000..12ed881 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwmoddatachecker.h @@ -0,0 +1,51 @@ +#ifndef FALLOUTTTW_MODATACHECKER_H +#define FALLOUTTTW_MODATACHECKER_H + +#include + +class FalloutTTWModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{"fonts", + "interface", + "menus", + "meshes", + "music", + "scripts", + "shaders", + "sound", + "strings", + "textures", + "trees", + "video", + "facegen", + "materials", + "nvse", + "distantlod", + "asi", + "Tools", + "MCM", + "distantland", + "mits", + "dllplugins", + "CalienteTools", + "shadersfx", + "config", + "KEYWORDS", + "BaseObjectSwapper", + "RaceMenuPresets"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // FALLOUTTTW_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/ttw/falloutttwmoddatacontent.h b/libs/game_bethesda/src/games/ttw/falloutttwmoddatacontent.h new file mode 100644 index 0000000..37f9d20 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwmoddatacontent.h @@ -0,0 +1,22 @@ +#ifndef FALLOUTTTW_MODDATACONTENT_H +#define FALLOUTTTW_MODDATACONTENT_H + +#include +#include + +class FalloutTTWModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + FalloutTTWModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_MCM] = false; + m_Enabled[CONTENT_SKYPROC] = false; + } +}; + +#endif // FALLOUTTTW_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp b/libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp new file mode 100644 index 0000000..9f41082 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp @@ -0,0 +1,79 @@ +#include "falloutttwsavegame.h" + +#include "gamefalloutttw.h" + +FalloutTTWSaveGame::FalloutTTWSaveGame(QString const& fileName, + GameFalloutTTW const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + uint32_t width, height; + fetchInformationFields(file, width, height, m_SaveNumber, m_PCName, m_PCLevel, + m_PCLocation); +} + +void FalloutTTWSaveGame::fetchInformationFields(FileWrapper& file, uint32_t& width, + uint32_t& height, + uint32_t& saveNumber, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation) const +{ + file.skip(); // Save header size + + file.skip(); // File version? + file.skip(); // Delimiter + + // A huge wodge of text with no length but a delimiter. Given the null bytes + // in it I presume it's fixed length (64 bytes + delim) but I have no + // definite spec + for (unsigned char ignore = 0; ignore != 0x7c;) { + file.read(ignore); // unknown + } + + file.setHasFieldMarkers(true); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BZSTRING); + + file.read(width); + file.read(height); + file.read(saveNumber); + file.read(playerName); + + QString whatthis; + file.read(whatthis); + + long level; + file.read(level); + playerLevel = level; + file.read(playerLocation); +} + +std::unique_ptr +FalloutTTWSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + + std::unique_ptr fields = std::make_unique(); + + uint32_t width, height; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + + fetchInformationFields(file, width, height, dummySaveNumber, dummyName, dummyLevel, + dummyLocation); + } + + QString playtime; + file.read(playtime); + + fields->Screenshot = file.readImage(width, height, 256); + + file.skip(5); // unknown (1 byte), plugin size (4 bytes) + + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/ttw/falloutttwsavegame.h b/libs/game_bethesda/src/games/ttw/falloutttwsavegame.h new file mode 100644 index 0000000..f8460b3 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwsavegame.h @@ -0,0 +1,23 @@ +#ifndef FALLOUTTTWSAVEGAME_H +#define FALLOUTTTWSAVEGAME_H + +#include "gamebryosavegame.h" + +class GameFalloutTTW; + +class FalloutTTWSaveGame : public GamebryoSaveGame +{ +public: + FalloutTTWSaveGame(QString const& fileName, GameFalloutTTW const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& width, + uint32_t& height, uint32_t& saveNumber, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation) const; + + std::unique_ptr fetchDataFields() const override; +}; + +#endif // FALLOUTTTWSAVEGAME_H diff --git a/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp b/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp new file mode 100644 index 0000000..ab9474e --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp @@ -0,0 +1,18 @@ +#include "falloutttwscriptextender.h" + +#include +#include + +FalloutTTWScriptExtender::FalloutTTWScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString FalloutTTWScriptExtender::BinaryName() const +{ + return "nvse_loader.exe"; +} + +QString FalloutTTWScriptExtender::PluginPath() const +{ + return "nvse/plugins"; +} diff --git a/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.h b/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.h new file mode 100644 index 0000000..d1e4c9a --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.h @@ -0,0 +1,17 @@ +#ifndef FALLOUTTTWSCRIPTEXTENDER_H +#define FALLOUTTTWSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class FalloutTTWScriptExtender : public GamebryoScriptExtender +{ +public: + FalloutTTWScriptExtender(const GameGamebryo* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // FALLOUTTTWSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/ttw/game_ttw_en.ts b/libs/game_bethesda/src/games/ttw/game_ttw_en.ts new file mode 100644 index 0000000..9d967f9 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/game_ttw_en.ts @@ -0,0 +1,22 @@ + + + + + GameFalloutTTW + + + Fallout TTW Support Plugin + + + + + Adds support for the game Fallout TTW + + + + + While not recommended by the TTW modding community, enables LOOT sorting + + + + diff --git a/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp b/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp new file mode 100644 index 0000000..34eb1b2 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp @@ -0,0 +1,364 @@ +#include "gamefalloutttw.h" + +#include "falloutttwbsainvalidation.h" +#include "falloutttwdataarchives.h" +#include "falloutttwmoddatachecker.h" +#include "falloutttwmoddatacontent.h" +#include "falloutttwsavegame.h" +#include "falloutttwscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace MOBase; + +GameFalloutTTW::GameFalloutTTW() {} + +bool GameFalloutTTW::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature( + std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "fallout.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +void GameFalloutTTW::setVariant(QString variant) +{ + m_GameVariant = variant; +} + +void GameFalloutTTW::checkVariants() +{ + QFileInfo gog_dll(m_GamePath + "\\Galaxy.dll"); + QFileInfo epic_dll(m_GamePath + "\\EOSSDK-Win32-Shipping.dll"); + if (gog_dll.exists()) + setVariant("GOG"); + else if (epic_dll.exists()) + setVariant("Epic Games"); + else + setVariant("Steam"); +} + +QDir GameFalloutTTW::documentsDirectory() const +{ + return m_MyGamesPath; +} + +QString GameFalloutTTW::identifyGamePath() const +{ + QString result; +#ifdef _WIN32 + QString path = "Software\\Bethesda Softworks\\FalloutNV"; + result = findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + result = GameGamebryo::identifyGamePath(); +#endif + // EPIC Game Store + if (result.isEmpty()) { + /** + * Basegame: 5daeb974a22a435988892319b3a4f476 + * Dead Money: b290229eb58045cbab9501640f3278f3 + * Honest Hearts: 562d4a2c1b3147b089a7c453e3ddbcbe + * Old World Blues: c8dae1ab0570475a8b38a9041e614840 + * Lonesome Road: 4fa3d8d9b2cb4714a19a38d1a598be8f + * Gun Runners' Arsenal: 7dcfb9cd9d134728b2646466c34c7b3b + * Courier's Stash: ee9a44b4530942499ef1c8c390731fce + */ + result = parseEpicGamesLocation({"5daeb974a22a435988892319b3a4f476"}); + if (QFileInfo(result).isDir()) { + QDir startPath = QDir(result); + auto subDirs = startPath.entryList({"Fallout New Vegas*"}, + QDir::Dirs | QDir::NoDotAndDotDot); + if (!subDirs.isEmpty()) + result += "/" + subDirs.first(); + } + } + return result; +} + +void GameFalloutTTW::setGamePath(const QString& path) +{ + m_GamePath = path; + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QDir GameFalloutTTW::savesDirectory() const +{ + return QDir(m_MyGamesPath + "/Saves"); +} + +QString GameFalloutTTW::myGamesPath() const +{ + return m_MyGamesPath; +} + +bool GameFalloutTTW::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +QString GameFalloutTTW::gameName() const +{ + return "TTW"; +} + +QString GameFalloutTTW::displayGameName() const +{ + return "Tale of Two Wastelands"; +} + +QString GameFalloutTTW::gameDirectoryName() const +{ + if (selectedVariant() == "Epic Games") + return "FalloutNV_Epic"; + else + return "FalloutNV"; +} + +void GameFalloutTTW::detectGame() +{ + m_GamePath = identifyGamePath(); + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QList GameFalloutTTW::executables() const +{ + ExecutableInfo game("Tale of Two Wastelands", findInGameFolder(binaryName())); + ExecutableInfo launcher("Fallout Launcher", findInGameFolder(getLauncherName())); + QList extraExecutables = + QList() << ExecutableInfo("GECK", findInGameFolder("geck.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"FalloutNV\""); + if (selectedVariant() != "Epic Games") { + extraExecutables.prepend(ExecutableInfo( + "NVSE", findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName()))); + } else { + game.withArgument("-EpicPortal"); + launcher.withArgument("-EpicPortal"); + } + QList executables = {game, launcher}; + executables += extraExecutables; + return executables; +} + +QList GameFalloutTTW::executableForcedLoads() const +{ + return QList(); +} + +QString GameFalloutTTW::name() const +{ + return "Fallout TTW Support Plugin"; +} + +QString GameFalloutTTW::localizedName() const +{ + return tr("Fallout TTW Support Plugin"); +} + +QString GameFalloutTTW::author() const +{ + return "SuperSandro2000 & MO2 Team"; +} + +QString GameFalloutTTW::description() const +{ + return tr("Adds support for the game Fallout TTW"); +} + +MOBase::VersionInfo GameFalloutTTW::version() const +{ + return VersionInfo(1, 6, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameFalloutTTW::settings() const +{ + return QList() + << PluginSetting("enable_loot_sorting", + tr("While not recommended by the TTW modding community, " + "enables LOOT sorting"), + false); +} + +void GameFalloutTTW::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/" + gameDirectoryName(), path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/fallout.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout_default.ini", + "fallout.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout.ini"); + } + + copyToProfile(myGamesPath(), path, "falloutprefs.ini"); + copyToProfile(myGamesPath(), path, "falloutcustom.ini"); + copyToProfile(myGamesPath(), path, "GECKCustom.ini"); + copyToProfile(myGamesPath(), path, "GECKPrefs.ini"); + } +} + +QString GameFalloutTTW::savegameExtension() const +{ + return "fos"; +} + +QString GameFalloutTTW::savegameSEExtension() const +{ + return "nvse"; +} + +std::shared_ptr +GameFalloutTTW::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFalloutTTW::steamAPPId() const +{ + if (selectedVariant() == "Steam") { + if (m_GamePath.endsWith("enplczru")) { + return "22490"; + } else { + return "22380"; + } + } + return QString(); +} + +QStringList GameFalloutTTW::primaryPlugins() const +{ + return {"falloutnv.esm", "deadmoney.esm", "honesthearts.esm", + "oldworldblues.esm", "lonesomeroad.esm", "gunrunnersarsenal.esm", + "fallout3.esm", "anchorage.esm", "thepitt.esm", + "brokensteel.esm", "pointlookout.esm", "zeta.esm", + "caravanpack.esm", "classicpack.esm", "mercenarypack.esm", + "tribalpack.esm", "taleoftwowastelands.esm", "YUPTTW.esm"}; +} + +QStringList GameFalloutTTW::gameVariants() const +{ + return {"Steam", "GOG", "Epic Games"}; +} + +QString GameFalloutTTW::binaryName() const +{ + return "FalloutNV.exe"; +} + +QString GameFalloutTTW::gameShortName() const +{ + return "TTW"; +} + +QStringList GameFalloutTTW::primarySources() const +{ + return {"FalloutNV"}; +} + +QStringList GameFalloutTTW::validShortNames() const +{ + return {"FalloutNV", "Fallout3"}; +} + +QString GameFalloutTTW::gameNexusName() const +{ + return ""; +} + +QStringList GameFalloutTTW::iniFiles() const +{ + return {"fallout.ini", "falloutprefs.ini", "falloutcustom.ini", "GECKCustom.ini", + "GECKPrefs.ini"}; +} + +QStringList GameFalloutTTW::DLCPlugins() const +{ + return {}; +} + +MOBase::IPluginGame::SortMechanism GameFalloutTTW::sortMechanism() const +{ + if (m_Organizer->pluginSetting(name(), "enable_loot_sorting").toBool()) + return IPluginGame::SortMechanism::LOOT; + return IPluginGame::SortMechanism::NONE; +} + +QString GameFalloutTTW::lootGameName() const +{ + return "FalloutNV"; +} + +int GameFalloutTTW::nexusModOrganizerID() const +{ + return 0; +} + +int GameFalloutTTW::nexusGameID() const +{ + return 0; +} + +QDir GameFalloutTTW::gameDirectory() const +{ + return QDir(m_GamePath); +} + +QString GameFalloutTTW::getLauncherName() const +{ + return "FalloutNVLauncher.exe"; +} + +MappingType GameFalloutTTW::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/FalloutNV/" + profileFile, false}); + if (selectedVariant() == "Epic Games") { + result.push_back( + {m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameDirectoryName() + "/" + profileFile, false}); + } + } + return result; +} diff --git a/libs/game_bethesda/src/games/ttw/gamefalloutttw.h b/libs/game_bethesda/src/games/ttw/gamefalloutttw.h new file mode 100644 index 0000000..7f989b0 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/gamefalloutttw.h @@ -0,0 +1,77 @@ +#ifndef GAMEFALLOUTTTW_H +#define GAMEFALLOUTTTW_H + +#include "gamegamebryo.h" + +#include +#include + +class GameFalloutTTW : public GameGamebryo +{ + Q_OBJECT +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.GameFalloutTTW" FILE "gamefalloutttw.json") +#endif + +public: + GameFalloutTTW(); + + bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual QString displayGameName() const override; + virtual void detectGame() override; + virtual QList executables() const override; + virtual QList + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString binaryName() const override; + virtual QString gameShortName() const override; + virtual QStringList validShortNames() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + virtual QStringList primarySources() const override; + virtual SortMechanism sortMechanism() const override; + virtual QString lootGameName() const override; + virtual QString getLauncherName() const override; + + virtual bool isInstalled() const override; + virtual void setGamePath(const QString& path) override; + virtual QDir gameDirectory() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +public: // IPluginFileMapper interface + virtual MappingType mappings() const override; + +protected: + QString gameDirectoryName() const; + QDir documentsDirectory() const; + QDir savesDirectory() const; + QString myGamesPath() const; + + void setVariant(QString variant); + void checkVariants(); + +protected: + virtual QString identifyGamePath() const override; + virtual QString savegameExtension() const override; + virtual QString savegameSEExtension() const override; + std::shared_ptr makeSaveGame(QString filePath) const override; +}; + +#endif // GAMEFALLOUTTTW_H diff --git a/libs/game_bethesda/src/games/ttw/gamefalloutttw.json b/libs/game_bethesda/src/games/ttw/gamefalloutttw.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/gamefalloutttw.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/vcpkg.json b/libs/game_bethesda/vcpkg.json new file mode 100644 index 0000000..2ee9a55 --- /dev/null +++ b/libs/game_bethesda/vcpkg.json @@ -0,0 +1,31 @@ +{ + "dependencies": [ + "lz4", + "zlib" + ], + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": [ + "mo2-cmake" + ] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673", + "packages": [ + "mo2-*" + ] + } + ] + } +} diff --git a/libs/game_features/.gitignore b/libs/game_features/.gitignore new file mode 100644 index 0000000..f5ebbdd --- /dev/null +++ b/libs/game_features/.gitignore @@ -0,0 +1,4 @@ +build +std*.log +CMakeLists.txt.user +vsbuild \ No newline at end of file diff --git a/libs/game_features/CMakeLists.txt b/libs/game_features/CMakeLists.txt new file mode 100644 index 0000000..7fd2e55 --- /dev/null +++ b/libs/game_features/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.16) +project(game_features) + +# Header-only interface library +# The canonical game feature headers live inside uibase's game_features/ subdirectory. +# We expose them here so game plugins can #include , , etc. +add_library(game_features INTERFACE) +target_include_directories(game_features INTERFACE + ${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase/game_features +) diff --git a/libs/game_features/src/CMakeLists.txt b/libs/game_features/src/CMakeLists.txt new file mode 100644 index 0000000..23d6313 --- /dev/null +++ b/libs/game_features/src/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) +if(DEFINED DEPENDENCIES_DIR) + include(${DEPENDENCIES_DIR}/modorganizer_super/cmake_common/src.cmake) +else() + include(../../cmake_common/src.cmake) +endif() \ No newline at end of file diff --git a/libs/game_features/src/bsainvalidation.h b/libs/game_features/src/bsainvalidation.h new file mode 100644 index 0000000..e2c9d9b --- /dev/null +++ b/libs/game_features/src/bsainvalidation.h @@ -0,0 +1,24 @@ +#ifndef BSAINVALIDATION_H +#define BSAINVALIDATION_H + +namespace MOBase { class IProfile; } + +class QString; + +class BSAInvalidation +{ +public: + + virtual ~BSAInvalidation() {} + + virtual bool isInvalidationBSA(const QString &bsaName) = 0; + + virtual void deactivate(MOBase::IProfile *profile) = 0; + + virtual void activate(MOBase::IProfile *profile) = 0; + + virtual bool prepareProfile(MOBase::IProfile *profile) = 0; + +}; + +#endif // BSAINVALIDATION_H diff --git a/libs/game_features/src/dataarchives.h b/libs/game_features/src/dataarchives.h new file mode 100644 index 0000000..26688ba --- /dev/null +++ b/libs/game_features/src/dataarchives.h @@ -0,0 +1,32 @@ +#ifndef DATAARCHIVES +#define DATAARCHIVES + +namespace MOBase { class IProfile; } + +class QString; +class QStringList; + +class DataArchives { + +public: + + virtual ~DataArchives() {} + + virtual QStringList vanillaArchives() const = 0; + + virtual QStringList archives(const MOBase::IProfile *profile) const = 0; + + /** + * @brief add an archive to the archive list + * @param profile the profile for which to change the archive list + * @param index index to insert before. 0 is the beginning of the list, INT_MAX can be used for the end of the list + * @param archiveName the archive to add + */ + virtual void addArchive(MOBase::IProfile *profile, int index, const QString &archiveName) = 0; + + virtual void removeArchive(MOBase::IProfile *profile, const QString &archiveName) = 0; + +}; + +#endif // DATAARCHIVES + diff --git a/libs/game_features/src/gamefeatures.pro b/libs/game_features/src/gamefeatures.pro new file mode 100644 index 0000000..1af5d6f --- /dev/null +++ b/libs/game_features/src/gamefeatures.pro @@ -0,0 +1,25 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2015-01-25T12:54:57 +# +#------------------------------------------------- + +QT -= gui + +TARGET = gamefeatures +TEMPLATE = lib + +CONFIG += staticlib + +DEFINES += GAMEFEATURES_LIBRARY + +SOURCES += + +HEADERS += \ + bsainvalidation.h \ + scriptextender.h \ + dataarchives.h \ + savegameinfo.h \ + localsavegames.h + +INCLUDEPATH += $${PWD}/../../uibase diff --git a/libs/game_features/src/gamefeatures_en.ts b/libs/game_features/src/gamefeatures_en.ts new file mode 100644 index 0000000..bc6d6e7 --- /dev/null +++ b/libs/game_features/src/gamefeatures_en.ts @@ -0,0 +1,4 @@ + + + + diff --git a/libs/game_features/src/gameplugins.h b/libs/game_features/src/gameplugins.h new file mode 100644 index 0000000..37ab112 --- /dev/null +++ b/libs/game_features/src/gameplugins.h @@ -0,0 +1,22 @@ +#ifndef GAMEPLUGINS_H +#define GAMEPLUGINS_H + +#include + +namespace MOBase { + class IPluginList; +} + + +class GamePlugins { + +public: + + virtual void writePluginLists(const MOBase::IPluginList *pluginList) = 0; + virtual void readPluginLists(MOBase::IPluginList *pluginList) = 0; + virtual QStringList getLoadOrder() = 0; + virtual bool lightPluginsAreSupported() = 0; + +}; + +#endif // GAMEPLUGINS_H diff --git a/libs/game_features/src/localsavegames.h b/libs/game_features/src/localsavegames.h new file mode 100644 index 0000000..86be073 --- /dev/null +++ b/libs/game_features/src/localsavegames.h @@ -0,0 +1,24 @@ +#ifndef LOCALSAVEGAMES_H +#define LOCALSAVEGAMES_H + + +#include "filemapping.h" + +namespace MOBase { + class IProfile; +} + +class QDir; + +class LocalSavegames { + +public: + + virtual ~LocalSavegames() {} + + virtual MappingType mappings(const QDir &profileSaveDir) const = 0; + virtual bool prepareProfile(MOBase::IProfile *profile) = 0; + +}; + +#endif // LOCALSAVEGAMES_H diff --git a/libs/game_features/src/moddatachecker.h b/libs/game_features/src/moddatachecker.h new file mode 100644 index 0000000..7c4c404 --- /dev/null +++ b/libs/game_features/src/moddatachecker.h @@ -0,0 +1,68 @@ +#ifndef MODDATACHECKER_H +#define MODDATACHECKER_H + +#include + +namespace MOBase { + + class IFileTree; + +}; + +class ModDataChecker { +public: + + /** + * + */ + enum class CheckReturn { + INVALID, + FIXABLE, + VALID + }; + + /** + * @brief Check that the given filetree represent a valid mod layout, or can be easily + * fixed. + * + * This method is mainly used during installation (to find which installer should + * be used or to recurse into multi-level archives), or to quickly indicates to a + * user if a mod looks valid. + * + * This method does not have to be exact, it only has to indicate if the given tree + * looks like a valid mod or not by quickly checking the structure (heavy operations + * should be avoided). + * + * If the tree can be fixed by the `fix()` method, this method should return `FIXABLE`. + * `FIXABLE` should only be returned when it is guaranteed that `fix()` can fix the tree. + * + * @param tree The tree starting at the root of the "data" folder. + * + * @return whether the tree is invalid, fixable or valid. + */ + virtual CheckReturn dataLooksValid(std::shared_ptr fileTree) const = 0; + + /** + * @brief Try to fix the given tree. + * + * This method is used during installation to try to fix invalid archives and will only be + * called if dataLooksValid returned `FIXABLE`. + * + * @param tree The tree to try to fix. Can be modified during the process. + * + * @return the fixed tree, or a null pointer if the tree could not be fixed. + */ + virtual std::shared_ptr fix(std::shared_ptr fileTree) const { + return nullptr; + } + +public: + + /** + * + */ + virtual ~ModDataChecker() { } + +}; + +#endif \ No newline at end of file diff --git a/libs/game_features/src/moddatacontent.h b/libs/game_features/src/moddatacontent.h new file mode 100644 index 0000000..e17bd66 --- /dev/null +++ b/libs/game_features/src/moddatacontent.h @@ -0,0 +1,112 @@ +#ifndef MODDATACONTENT_H +#define MODDATACONTENT_H + +#include +#include +#include + +#include + +namespace MOBase { + + class IFileTree; + +}; + +/** + * The ModDataContent feature is used (when available) to indicate to users the content + * of mods in the "Content" column. + * + * The feature exposes a list of possible content types, each associated with an ID, a name + * and an icon. The icon is the path to either: + * - A Qt resource or; + * - A file on the disk. + * + * In order to facilitate the implementation, MO2 already provides a set of icons that can + * be used. Those icons are all under :/MO/gui/content (e.g. :/MO/gui/content/plugin or :/MO/gui/content/music). + * + * The list of available icons is: + * - plugin: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/jigsaw-piece.png + * - skyproc: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/hand-of-god.png + * - texture: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/empty-chessboard.png + * - music: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/double-quaver.png + * - sound: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/lyre.png + * - interface: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/usable.png + * - skse: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/checkbox-tree.png + * - script: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/tinker.png + * - mesh: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/breastplate.png + * - string: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/conversation.png + * - bsa: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/locked-chest.png + * - menu: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/config.png + * - inifile: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/feather-and-scroll.png + * - modgroup: https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/xedit.png + */ +class ModDataContent { +public: + + struct Content { + + /** + * @param id ID of this content. + * @param name Name of this content. + * @param icon Path to the icon for this content. Can be either a path + * to an image on the disk, or to a resource. Can be an empty string if filterOnly + * is true. + * @param filterOnly Indicates if the content should only be show in the filter + * criteria and not in the actual Content column. + */ + Content(int id, QString name, QString icon, bool filterOnly = false) : + m_Id{ id }, m_Name{ name }, m_Icon{ icon }, m_FilterOnly{ filterOnly } { } + + /** + * @return the ID of this content. + */ + int id() const { return m_Id; } + + /** + * @return the name of this content. + */ + QString name() const { return m_Name; } + + /** + * @return the path to the icon of this content (can be a Qt resource path). + */ + QString icon() const { return m_Icon; } + + /** + * @return true if this content is only meant to be used as a filter criteria. + */ + bool isOnlyForFilter() const { return m_FilterOnly; } + + private: + + int m_Id; + QString m_Name; + QString m_Icon; + bool m_FilterOnly; + }; + + /** + * @return the list of all possible contents for the corresponding game. + */ + virtual std::vector getAllContents() const = 0; + + /** + * @brief Retrieve the list of contents in the given tree. + * + * @param fileTree The tree corresponding to the mod to retrieve contents for. + * + * @return the IDs of the content in the given tree. + */ + virtual std::vector getContentsFor(std::shared_ptr fileTree) const = 0; + +public: + + /** + * + */ + virtual ~ModDataContent() { } + +}; + +#endif \ No newline at end of file diff --git a/libs/game_features/src/savegameinfo.h b/libs/game_features/src/savegameinfo.h new file mode 100644 index 0000000..10edaad --- /dev/null +++ b/libs/game_features/src/savegameinfo.h @@ -0,0 +1,49 @@ +#ifndef SAVEGAMEINFO_H +#define SAVEGAMEINFO_H + +namespace MOBase { class ISaveGame; } +namespace MOBase { class ISaveGameInfoWidget; } + +#include + +class QString; +class QStringList; +class QWidget; + +/** Feature to get hold of stuff to do with save games */ +class SaveGameInfo +{ +public: + virtual ~SaveGameInfo() {} + + typedef QStringList ProvidingModules; + typedef QMap MissingAssets; + + /** + * @brief Get missing items from a save. + * + * @param save The save to retrieve missing assets from. + * + * @returns a collection of missing assets and the modules that can supply those + * assets. + * + * Note that in the situation where 'module' and 'asset' are indistinguishable, + * both still have to be supplied. + */ + virtual MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const = 0; + + /** + * @brief Get a widget to display over the save game list. + * + * @param parent The parent widget. + * + * @returns a Qt widget to display saves Widget. + * + * It is permitted to return a null pointer to indicate the plugin does not have a + * nice visual way of displaying save game contents. + */ + virtual MOBase::ISaveGameInfoWidget *getSaveGameWidget(QWidget *parent = 0) const = 0; +}; + +#endif // SAVEGAMEINFO_H + diff --git a/libs/game_features/src/scriptextender.h b/libs/game_features/src/scriptextender.h new file mode 100644 index 0000000..1f91df6 --- /dev/null +++ b/libs/game_features/src/scriptextender.h @@ -0,0 +1,41 @@ +#ifndef SCRIPTEXTENDER +#define SCRIPTEXTENDER + +#include + +class QString; +class QStringList; + +class ScriptExtender { + +public: + + virtual ~ScriptExtender() {} + + /** Get the name of the script extender binary */ + virtual QString BinaryName() const = 0; + + /** Get the script extender plugin path*/ + virtual QString PluginPath() const = 0; + + /** The loader to use to ensure the game runs with the script extender */ + virtual QString loaderName() const = 0; + + /** Full path of the loader */ + virtual QString loaderPath() const = 0; + + /** Extension of the script extender save game */ + virtual QString savegameExtension() const = 0; + + /** Returns true if the extender is installed */ + virtual bool isInstalled() const = 0; + + /** Get version of extender */ + virtual QString getExtenderVersion() const = 0; + + /** Get CPU platform of extender */ + virtual WORD getArch() const = 0; +}; + +#endif // SCRIPTEXTENDER + diff --git a/libs/game_features/src/unmanagedmods.h b/libs/game_features/src/unmanagedmods.h new file mode 100644 index 0000000..545b369 --- /dev/null +++ b/libs/game_features/src/unmanagedmods.h @@ -0,0 +1,44 @@ +#ifndef UNMANAGEDMODS_H +#define UNMANAGEDMODS_H + +class QString; +class QFileInfo; +class QStringList; + + +class UnmanagedMods { + +public: + +public: + + virtual ~UnmanagedMods() {} + + /** + * @param onlyOfficial if set, only official mods (dlcs) are returned + * @return the list of unmanaged mods (internal names) + */ + virtual QStringList mods(bool onlyOfficial) const = 0; + + /** + * @param modName (internal) name of the mod being requested + * @return display name of the mod + */ + virtual QString displayName(const QString &modName) const = 0; + + /** + * @param modName name of the mod being requested + * @return reference file info + */ + virtual QFileInfo referenceFile(const QString &modName) const = 0; + + /** + * @param modName name of the mod being requested + * @return list of file names (absolute paths) + */ + virtual QStringList secondaryFiles(const QString &modName) const = 0; + +}; + +#endif // UNMANAGEDMODS_H + diff --git a/libs/game_gamebryo/.clang-format b/libs/game_gamebryo/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/game_gamebryo/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/game_gamebryo/.git-blame-ignore-revs b/libs/game_gamebryo/.git-blame-ignore-revs new file mode 100644 index 0000000..7db8755 --- /dev/null +++ b/libs/game_gamebryo/.git-blame-ignore-revs @@ -0,0 +1 @@ +f4c5c5535551ed085bd65cb02058dcab2c9eb110 diff --git a/libs/game_gamebryo/.gitattributes b/libs/game_gamebryo/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/game_gamebryo/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/game_gamebryo/.github/workflows/build.yml b/libs/game_gamebryo/.github/workflows/build.yml new file mode 100644 index 0000000..7efc43f --- /dev/null +++ b/libs/game_gamebryo/.github/workflows/build.yml @@ -0,0 +1,17 @@ +name: Build GameBryo Library + +on: + push: + branches: master + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build GameBryo Library + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-third-parties: lz4 zlib + mo2-dependencies: cmake_common uibase diff --git a/libs/game_gamebryo/.github/workflows/linting.yml b/libs/game_gamebryo/.github/workflows/linting.yml new file mode 100644 index 0000000..1b8fe2a --- /dev/null +++ b/libs/game_gamebryo/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint GameBryo Library + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/game_gamebryo/.gitignore b/libs/game_gamebryo/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/game_gamebryo/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/game_gamebryo/CMakeLists.txt b/libs/game_gamebryo/CMakeLists.txt new file mode 100644 index 0000000..5b7ffba --- /dev/null +++ b/libs/game_gamebryo/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.16) + +if(DEFINED DEPENDENCIES_DIR) + include(${DEPENDENCIES_DIR}/modorganizer_super/cmake_common/mo2.cmake) +else() + include(${CMAKE_CURRENT_LIST_DIR}/../cmake_common/mo2.cmake) +endif() + +project(game_gamebryo) + +add_subdirectory(src/gamebryo) +add_subdirectory(src/creation) diff --git a/libs/game_gamebryo/README.md b/libs/game_gamebryo/README.md new file mode 100644 index 0000000..08b5eb4 --- /dev/null +++ b/libs/game_gamebryo/README.md @@ -0,0 +1,5 @@ +# Archived repository - Check [modorganizer-game_bethesda](https://github.com/ModOrganizer2/modorganizer-game_bethesda) + + +ModOrganizer2 plugins for most Bethesda games are now managed in a single repository +at https://github.com/ModOrganizer2/modorganizer-game_bethesda. diff --git a/libs/game_gamebryo/src/creation/CMakeLists.txt b/libs/game_gamebryo/src/creation/CMakeLists.txt new file mode 100644 index 0000000..f2dce97 --- /dev/null +++ b/libs/game_gamebryo/src/creation/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_creation STATIC) +mo2_configure_library(game_creation + WARNINGS OFF + TRANSLATIONS ON + PUBLIC_DEPENDS uibase + PRIVATE_DEPENDS lz4) +target_link_libraries(game_creation PUBLIC game_gamebryo) +mo2_install_target(game_creation) diff --git a/libs/game_gamebryo/src/creation/creationgameplugins.cpp b/libs/game_gamebryo/src/creation/creationgameplugins.cpp new file mode 100644 index 0000000..f5cef62 --- /dev/null +++ b/libs/game_gamebryo/src/creation/creationgameplugins.cpp @@ -0,0 +1,183 @@ +#include "creationgameplugins.h" +#include +#include +#include +#include + +#include +#include +#include +#include + +using MOBase::IOrganizer; +using MOBase::IPluginGame; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +CreationGamePlugins::CreationGamePlugins(IOrganizer* organizer) + : GamebryoGamePlugins(organizer) +{} + +QStringList CreationGamePlugins::getLoadOrder() +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (loadOrderIsNew || !pluginsIsNew) { + return readLoadOrderList(m_Organizer->pluginList(), loadOrderPath); + } else { + return readPluginList(m_Organizer->pluginList()); + } +} + +void CreationGamePlugins::writePluginList(const IPluginList* pluginList, + const QString& filePath) +{ + SafeWriteFile file(filePath); + + QStringEncoder encoder(QStringConverter::Encoding::System); + + file->resize(0); + + file->write( + encoder.encode("# This file was automatically generated by Mod Organizer.\r\n")); + + bool invalidFileNames = false; + int writtenCount = 0; + + QStringList plugins = pluginList->pluginNames(); + std::sort(plugins.begin(), plugins.end(), + [pluginList](const QString& lhs, const QString& rhs) { + return pluginList->priority(lhs) < pluginList->priority(rhs); + }); + + QStringList PrimaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList DLCPlugins = organizer()->managedGame()->DLCPlugins(); + QSet ManagedMods = + QSet(PrimaryPlugins.begin(), PrimaryPlugins.end()); + QSet DLCSet = QSet(DLCPlugins.begin(), DLCPlugins.end()); + ManagedMods.subtract(DLCSet); + PrimaryPlugins.append(QList(ManagedMods.begin(), ManagedMods.end())); + + // TODO: do not write plugins in OFFICIAL_FILES container + for (const QString& pluginName : plugins) { + if (!PrimaryPlugins.contains(pluginName, Qt::CaseInsensitive)) { + if (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE) { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write("*"); + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } else { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } + } + } + + if (invalidFileNames) { + reportError(QObject::tr("Some of your plugins have invalid names! These " + "plugins can not be loaded by the game. Please see " + "mo_interface.log for a list of affected plugins " + "and rename them.")); + } + + file->commit(); +} + +QStringList CreationGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + const auto plugins = pluginList->pluginNames(); + const auto primaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList loadOrder(primaryPlugins); + + for (const QString& pluginName : loadOrder) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + qWarning("%s not found", qUtf8Printable(filePath)); + return loadOrder; + } + ON_BLOCK_EXIT([&]() { + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + qWarning("%s empty", qUtf8Printable(filePath)); + return loadOrder; + } + + QStringList pluginsFound; + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + pluginName = QStringEncoder(QStringConverter::Encoding::System) + .encode(line.trimmed().constData()); + } + if (!primaryPlugins.contains(pluginName, Qt::CaseInsensitive)) { + if (pluginName.startsWith('*')) { + pluginName.remove(0, 1); + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + pluginsFound.append(pluginName); + if (!loadOrder.contains(pluginName, Qt::CaseInsensitive)) { + loadOrder.append(pluginName); + } + } + } else { + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + pluginsFound.append(pluginName); + if (!loadOrder.contains(pluginName, Qt::CaseInsensitive)) { + loadOrder.append(pluginName); + } + } + } + } else { + pluginName.remove(0, 1); + pluginsFound.append(pluginName); + } + } + + file.close(); + + // set all plugins not found inactive + for (const auto& pluginName : plugins) { + if (!pluginsFound.contains(pluginName, Qt::CaseInsensitive)) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return loadOrder; +} + +bool CreationGamePlugins::lightPluginsAreSupported() +{ + return true; +} diff --git a/libs/game_gamebryo/src/creation/creationgameplugins.h b/libs/game_gamebryo/src/creation/creationgameplugins.h new file mode 100644 index 0000000..f7afd80 --- /dev/null +++ b/libs/game_gamebryo/src/creation/creationgameplugins.h @@ -0,0 +1,22 @@ +#ifndef CREATIONGAMEPLUGINS_H +#define CREATIONGAMEPLUGINS_H + +#include +#include +#include +#include + +class CreationGamePlugins : public GamebryoGamePlugins +{ +public: + CreationGamePlugins(MOBase::IOrganizer* organizer); + +protected: + virtual void writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) override; + virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override; + virtual QStringList getLoadOrder() override; + virtual bool lightPluginsAreSupported() override; +}; + +#endif // CREATIONGAMEPLUGINS_H diff --git a/libs/game_gamebryo/src/creation/game_creation_en.ts b/libs/game_gamebryo/src/creation/game_creation_en.ts new file mode 100644 index 0000000..f217448 --- /dev/null +++ b/libs/game_gamebryo/src/creation/game_creation_en.ts @@ -0,0 +1,12 @@ + + + + + QObject + + + Some of your plugins have invalid names! These plugins can not be loaded by the game. Please see mo_interface.log for a list of affected plugins and rename them. + + + + diff --git a/libs/game_gamebryo/src/gamebryo/CMakeLists.txt b/libs/game_gamebryo/src/gamebryo/CMakeLists.txt new file mode 100644 index 0000000..cff88fc --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_gamebryo STATIC) +mo2_configure_library(game_gamebryo + WARNINGS OFF + TRANSLATIONS ON + AUTOMOC ON + PUBLIC_DEPENDS uibase + PRIVATE_DEPENDS zlib lz4) +mo2_install_target(game_gamebryo) diff --git a/libs/game_gamebryo/src/gamebryo/dummybsa.cpp b/libs/game_gamebryo/src/gamebryo/dummybsa.cpp new file mode 100644 index 0000000..ba6f766 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/dummybsa.cpp @@ -0,0 +1,192 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "dummybsa.h" +#include +#define WIN32_LEAN_AND_MEAN +#include + +static void writeUlong(unsigned char* buffer, int offset, unsigned long value) +{ + union + { + unsigned long ulValue; + unsigned char cValue[4]; + }; + ulValue = value; + memcpy(buffer + offset, cValue, 4); +} + +static void writeUlonglong(unsigned char* buffer, int offset, unsigned long long value) +{ + union + { + unsigned long long ullValue; + unsigned char cValue[8]; + }; + ullValue = value; + memcpy(buffer + offset, cValue, 8); +} + +static unsigned long genHashInt(const unsigned char* pos, const unsigned char* end) +{ + unsigned long hash = 0; + for (; pos < end; ++pos) { + hash *= 0x1003f; + hash += *pos; + } + return hash; +} + +static unsigned long long genHash(const char* fileName) +{ + char fileNameLower[MAX_PATH + 1]; + int i = 0; + for (; i < MAX_PATH && fileName[i] != '\0'; ++i) { + fileNameLower[i] = static_cast(tolower(fileName[i])); + if (fileNameLower[i] == '\\') { + fileNameLower[i] = '/'; + } + } + fileNameLower[i] = '\0'; + + unsigned char* fileNameLowerU = reinterpret_cast(fileNameLower); + + char* ext = strrchr(fileNameLower, '.'); + if (ext == nullptr) { + ext = fileNameLower + strlen(fileNameLower); + } + unsigned char* extU = reinterpret_cast(ext); + + int length = ext - fileNameLower; + + unsigned long long hash = 0ULL; + + if (length > 0) { + hash = *(extU - 1) | ((length > 2 ? *(ext - 2) : 0) << 8) | (length << 16) | + (fileNameLowerU[0] << 24); + } + + if (strlen(ext) > 0) { + if (strcmp(ext + 1, "kf") == 0) { + hash |= 0x80; + } else if (strcmp(ext + 1, "nif") == 0) { + hash |= 0x8000; + } else if (strcmp(ext + 1, "dds") == 0) { + hash |= 0x8080; + } else if (strcmp(ext + 1, "wav") == 0) { + hash |= 0x80000000; + } + + unsigned long long temp = + static_cast(genHashInt(fileNameLowerU + 1, extU - 2)); + temp += static_cast(genHashInt(extU, extU + strlen(ext))); + + hash |= (temp & 0xFFFFFFFF) << 32; + } + return hash; +} + +DummyBSA::DummyBSA(unsigned long bsaVersion) + : m_Version(bsaVersion), m_FolderName(""), m_FileName("dummy.dds"), + m_TotalFileNameLength(0) +{} + +void DummyBSA::writeHeader(QFile& file) +{ + unsigned char header[] = { + 'B', 'S', 'A', '\0', // magic string + 0xDE, 0xAD, 0xBE, 0xEF, // version - insert later + 0x24, 0x00, 0x00, 0x00, // offset to folder recors. header size is static + 0xDE, 0xAD, 0xBE, 0xEF, // archive flags - insert later + 0x01, 0x00, 0x00, 0x00, // folder count + 0x01, 0x00, 0x00, 0x00, // file count + 0xDE, 0xAD, 0xBE, 0xEF, // total folder names length - insert later + 0xDE, 0xAD, 0xBE, 0xEF, // total file names length - insert later + 0xDE, 0xAD, 0xBE, 0xEF // file flags - insert later + }; + + writeUlong(header, 4, m_Version); + writeUlong(header, 12, 0x01 | 0x02); // has directories and has files. + writeUlong(header, 24, + static_cast(m_FolderName.length()) + + 1); // empty folder name + writeUlong(header, 28, m_TotalFileNameLength); // single character file name + + writeUlong(header, 32, 2); // has dds + + file.write(reinterpret_cast(header), sizeof(header)); +} + +void DummyBSA::writeFolderRecord(QFile& file, const std::string& folderName) +{ + unsigned char folderRecord[] = { + 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, // folder hash + 0x01, 0x00, 0x00, 0x00, // file count + 0xDE, 0xAD, 0xBE, 0xEF, // offset to folder name + }; + // we'd usually have to sort folders be the hash value generated here + writeUlonglong(folderRecord, 0, genHash(folderName.c_str())); + writeUlong(folderRecord, 12, + 0x34 + m_TotalFileNameLength); // TODO: this should be calculated properly + + file.write(reinterpret_cast(folderRecord), sizeof(folderRecord)); +} + +void DummyBSA::writeFileRecord(QFile& file, const std::string& fileName) +{ + unsigned char fileRecord[] = { + 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, // file name hash + 0xDE, 0xAD, 0xBE, 0xEF, // size + 0xDE, 0xAD, 0xBE, 0xEF, // offset to file data + }; + + // we'd usually have to sort files by the value generated here + writeUlonglong(fileRecord, 0, genHash(fileName.c_str())); + writeUlong(fileRecord, 8, 0); + writeUlong( + fileRecord, 12, + 0x44 + static_cast(fileName.length() + 1) + + 4); // after this record we expect the filename and 4 bytes of file size + + file.write(reinterpret_cast(fileRecord), sizeof(fileRecord)); +} + +void DummyBSA::writeFileRecordBlocks(QFile& file, const std::string& folderName) +{ + file.write(folderName.c_str(), folderName.length() + 1); + + writeFileRecord(file, m_FileName); +} + +void DummyBSA::write(const QString& fileName) +{ + QFile file(fileName); + file.open(QIODevice::WriteOnly); + + m_TotalFileNameLength = static_cast(m_FileName.length() + 1); + + writeHeader(file); + writeFolderRecord(file, m_FolderName); + writeFileRecordBlocks(file, m_FolderName); + file.write(m_FileName.c_str(), m_FileName.length() + 1); + char fileSize[] = {0x00, 0x00, 0x00, 0x00}; + file.write(fileSize, sizeof(fileSize)); + file.close(); +} diff --git a/libs/game_gamebryo/src/gamebryo/dummybsa.h b/libs/game_gamebryo/src/gamebryo/dummybsa.h new file mode 100644 index 0000000..8ea071b --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/dummybsa.h @@ -0,0 +1,59 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef DUMMYBSA_H +#define DUMMYBSA_H + +#include +#include + +/** + * @brief Class for creating a dummy bsa used for archive invalidation + **/ +class DummyBSA +{ + +public: + /** + * @brief constructor + * + **/ + DummyBSA(unsigned long bsaVersion); + + /** + * @brief write to the specified file + * + * @param fileName name of the file to write to + **/ + void write(const QString& fileName); + +private: + void writeHeader(QFile& file); + void writeFolderRecord(QFile& file, const std::string& folderName); + void writeFileRecord(QFile& file, const std::string& fileName); + void writeFileRecordBlocks(QFile& file, const std::string& folderName); + +private: + unsigned long m_Version; + std::string m_FolderName; + std::string m_FileName; + unsigned long m_TotalFileNameLength; +}; + +#endif // DUMMYBSA_H diff --git a/libs/game_gamebryo/src/gamebryo/game_gamebryo_en.ts b/libs/game_gamebryo/src/gamebryo/game_gamebryo_en.ts new file mode 100644 index 0000000..f924131 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/game_gamebryo_en.ts @@ -0,0 +1,170 @@ + + + + + GamebryoModDataContent + + + Plugins (ESP/ESM/ESL) + + + + + Optional Plugins + + + + + Interface + + + + + Meshes + + + + + Bethesda Archive + + + + + Scripts (Papyrus) + + + + + Script Extender Plugin + + + + + Script Extender Files + + + + + SkyProc Patcher + + + + + Sound or Music + + + + + Textures + + + + + MCM Configuration + + + + + INI Files + + + + + FaceGen Data + + + + + ModGroup Files + + + + + GamebryoSaveGameInfoWidget + + + Save # + + + + + Character + + + + + Level + + + + + Location + + + + + Date + + + + + Has Script Extender Data + + + + + Missing ESPs + + + + + + + None + + + + + Missing ESHs + + + + + Missing ESLs + + + + + QObject + + + Some of your plugins have invalid names! These plugins can not be loaded by the game. Please see mo_interface.log for a list of affected plugins and rename them. + + + + + %1, #%2, Level %3, %4 + + + + + failed to open %1 + + + + + wrong file format - expected %1 got '%2' for %3 + + + + + failed to query registry path (preflight): %1 + + + + + failed to query registry path (read): %1 + + + + diff --git a/libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.cpp b/libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.cpp new file mode 100644 index 0000000..1b7144b --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.cpp @@ -0,0 +1,133 @@ +#include "gamebryobsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include +#include + +#include +#include + +#include + +GamebryoBSAInvalidation::GamebryoBSAInvalidation(MOBase::DataArchives* dataArchives, + const QString& iniFilename, + MOBase::IPluginGame const* game) + : m_DataArchives(dataArchives), m_IniFileName(iniFilename), m_Game(game) +{} + +bool GamebryoBSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + static QStringList invalidation{invalidationBSAName()}; + + for (const QString& file : invalidation) { + if (file.compare(bsaName, Qt::CaseInsensitive) == 0) { + return true; + } + } + return false; +} + +void GamebryoBSAInvalidation::deactivate(MOBase::IProfile* profile) +{ + prepareProfile(profile); +} + +void GamebryoBSAInvalidation::activate(MOBase::IProfile* profile) +{ + prepareProfile(profile); +} + +bool GamebryoBSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + WCHAR setting[MAX_PATH]; + + // write bInvalidateOlderFiles = 1, if needed + if (!::GetPrivateProfileStringW(L"Archive", L"bInvalidateOlderFiles", L"0", setting, + MAX_PATH, iniFilePath.toStdWString().c_str()) || + wcstol(setting, nullptr, 10) != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue(L"Archive", L"bInvalidateOlderFiles", L"1", + iniFilePath.toStdWString().c_str())) { + qWarning("failed to activate BSA invalidation in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + + if (profile->invalidationActive(nullptr)) { + + // add the dummy bsa to the archive string, if needed + QStringList archives = m_DataArchives->archives(profile); + bool bsaInstalled = false; + for (const QString& archive : archives) { + if (isInvalidationBSA(archive)) { + bsaInstalled = true; + break; + } + } + if (!bsaInstalled) { + m_DataArchives->addArchive(profile, 0, invalidationBSAName()); + dirty = true; + } + + // create the dummy bsa if necessary + QString bsaFile = m_Game->dataDirectory().absoluteFilePath(invalidationBSAName()); + if (!QFile::exists(bsaFile)) { + DummyBSA bsa(bsaVersion()); + bsa.write(bsaFile); + dirty = true; + } + + // write SInvalidationFile = "", if needed + if (::GetPrivateProfileStringW(L"Archive", L"SInvalidationFile", + L"ArchiveInvalidation.txt", setting, MAX_PATH, + iniFilePath.toStdWString().c_str()) || + wcscmp(setting, L"") != 0) { + dirty = true; + if (!MOBase::WriteRegistryValue(L"Archive", L"SInvalidationFile", L"", + iniFilePath.toStdWString().c_str())) { + qWarning("failed to activate BSA invalidation in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } else { + + // remove the dummy bsa from the archive string, if needed + QStringList archivesBefore = m_DataArchives->archives(profile); + for (const QString& archive : archivesBefore) { + if (isInvalidationBSA(archive)) { + m_DataArchives->removeArchive(profile, archive); + dirty = true; + } + } + + // delete the dummy bsa, if needed + QString bsaFile = m_Game->dataDirectory().absoluteFilePath(invalidationBSAName()); + if (QFile::exists(bsaFile)) { + MOBase::shellDeleteQuiet(bsaFile); + dirty = true; + } + + // write SInvalidationFile = "ArchiveInvalidation.txt", if needed + if (!::GetPrivateProfileStringW(L"Archive", L"SInvalidationFile", L"", setting, + MAX_PATH, iniFilePath.toStdWString().c_str()) || + wcscmp(setting, L"ArchiveInvalidation.txt") != 0) { + dirty = true; + if (!MOBase::WriteRegistryValue(L"Archive", L"SInvalidationFile", + L"ArchiveInvalidation.txt", + iniFilePath.toStdWString().c_str())) { + qWarning("failed to activate BSA invalidation in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.h b/libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.h new file mode 100644 index 0000000..efd3d7b --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.h @@ -0,0 +1,36 @@ +#ifndef GAMEBRYOBSAINVALIDATION_H +#define GAMEBRYOBSAINVALIDATION_H + +#include +#include +#include +#include + +namespace MOBase +{ +class IPluginGame; +} + +class GamebryoBSAInvalidation : public MOBase::BSAInvalidation +{ +public: + GamebryoBSAInvalidation(MOBase::DataArchives* dataArchives, + const QString& iniFilename, MOBase::IPluginGame const* game); + + virtual bool isInvalidationBSA(const QString& bsaName) override; + virtual void deactivate(MOBase::IProfile* profile) override; + virtual void activate(MOBase::IProfile* profile) override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +private: + virtual QString invalidationBSAName() const = 0; + virtual unsigned long + bsaVersion() const = 0; // 0x67 for oblivion, 0x68 for everything else + +private: + MOBase::DataArchives* m_DataArchives; + QString m_IniFileName; + MOBase::IPluginGame const* m_Game; +}; + +#endif // GAMEBRYOBSAINVALIDATION_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryodataarchives.cpp b/libs/game_gamebryo/src/gamebryo/gamebryodataarchives.cpp new file mode 100644 index 0000000..3dcba8a --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryodataarchives.cpp @@ -0,0 +1,80 @@ +#include "gamebryodataarchives.h" + +#include + +#include +#include + +#include "gamegamebryo.h" + +GamebryoDataArchives::GamebryoDataArchives(const GameGamebryo* game) : m_Game{game} {} + +QDir GamebryoDataArchives::gameDirectory() const +{ + return QDir(m_Game->gameDirectory()).absolutePath(); +} + +QDir GamebryoDataArchives::localGameDirectory() const +{ + return QDir(m_Game->myGamesPath()).absolutePath(); +} + +QStringList GamebryoDataArchives::getArchivesFromKey(const QString& iniFile, + const QString& key, + const int size) const +{ + wchar_t* buffer = new wchar_t[size]; + QStringList result; + std::wstring iniFileW = QDir::toNativeSeparators(iniFile).toStdWString(); + + // epic ms fail: GetPrivateProfileString uses errno (for whatever reason) to signal a + // fail since the return value has a different meaning (number of bytes copied). + // HOWEVER, it will not set errno to 0 if NO error occured + errno = 0; + + if (::GetPrivateProfileStringW(L"Archive", key.toStdWString().c_str(), L"", buffer, + size, iniFileW.c_str()) != 0) { + result.append(QString::fromStdWString(buffer).split(',')); + } + + for (int i = 0; i < result.count(); ++i) { + result[i] = result[i].trimmed(); + } + delete[] buffer; + return result; +} + +void GamebryoDataArchives::setArchivesToKey(const QString& iniFile, const QString& key, + const QString& value) +{ + if (!MOBase::WriteRegistryValue(L"Archive", key.toStdWString().c_str(), + value.toStdWString().c_str(), + iniFile.toStdWString().c_str())) { + qWarning("failed to set archives in \"%s\"", qUtf8Printable(iniFile)); + } +} + +void GamebryoDataArchives::addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) +{ + QStringList current = archives(profile); + if (current.contains(archiveName, Qt::CaseInsensitive)) { + return; + } + + current.insert(index != INT_MAX ? index : current.size(), archiveName); + + writeArchiveList(profile, current); +} + +void GamebryoDataArchives::removeArchive(MOBase::IProfile* profile, + const QString& archiveName) +{ + QStringList current = archives(profile); + if (!current.contains(archiveName, Qt::CaseInsensitive)) { + return; + } + current.removeAll(archiveName); + + writeArchiveList(profile, current); +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryodataarchives.h b/libs/game_gamebryo/src/gamebryo/gamebryodataarchives.h new file mode 100644 index 0000000..13f4130 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryodataarchives.h @@ -0,0 +1,37 @@ +#ifndef GAMEBRYODATAARCHIVES_H +#define GAMEBRYODATAARCHIVES_H + +#include + +#include "dataarchives.h" + +class GameGamebryo; + +class GamebryoDataArchives : public MOBase::DataArchives +{ + +public: + GamebryoDataArchives(const GameGamebryo* game); + + virtual void addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) override; + virtual void removeArchive(MOBase::IProfile* profile, + const QString& archiveName) override; + +protected: + QDir gameDirectory() const; + QDir localGameDirectory() const; + + QStringList getArchivesFromKey(const QString& iniFile, const QString& key, + int size = 256) const; + void setArchivesToKey(const QString& iniFile, const QString& key, + const QString& value); + +private: + const GameGamebryo* m_Game; + + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) = 0; +}; + +#endif // GAMEBRYODATAARCHIVES_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryogameplugins.cpp b/libs/game_gamebryo/src/gamebryo/gamebryogameplugins.cpp new file mode 100644 index 0000000..83ad50f --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryogameplugins.cpp @@ -0,0 +1,248 @@ +#include "gamebryogameplugins.h" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using MOBase::IOrganizer; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +GamebryoGamePlugins::GamebryoGamePlugins(IOrganizer* organizer) : m_Organizer(organizer) +{} + +void GamebryoGamePlugins::writePluginLists(const IPluginList* pluginList) +{ + if (!m_LastRead.isValid()) { + // attempt to write uninitialized plugin lists + return; + } + + writePluginList(pluginList, m_Organizer->profile()->absolutePath() + "/plugins.txt"); + writeLoadOrderList(pluginList, + m_Organizer->profile()->absolutePath() + "/loadorder.txt"); + + m_LastRead = QDateTime::currentDateTime(); +} + +void GamebryoGamePlugins::readPluginLists(MOBase::IPluginList* pluginList) +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (loadOrderIsNew || !pluginsIsNew) { + // read both files if they are both new or both older than the last read + QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); + pluginList->setLoadOrder(loadOrder); + readPluginList(pluginList); + } else { + // If the plugins is new but not loadorder, we must reparse the load order from the + // plugin files + QStringList loadOrder = readPluginList(pluginList); + pluginList->setLoadOrder(loadOrder); + } + + m_LastRead = QDateTime::currentDateTime(); +} + +QStringList GamebryoGamePlugins::getLoadOrder() +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (loadOrderIsNew || !pluginsIsNew) { + return readLoadOrderList(m_Organizer->pluginList(), loadOrderPath); + } else { + return readPluginList(m_Organizer->pluginList()); + } +} + +void GamebryoGamePlugins::writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) +{ + return writeList(pluginList, filePath, false); +} + +void GamebryoGamePlugins::writeLoadOrderList(const MOBase::IPluginList* pluginList, + const QString& filePath) +{ + return writeList(pluginList, filePath, true); +} + +void GamebryoGamePlugins::writeList(const IPluginList* pluginList, + const QString& filePath, bool loadOrder) +{ + SafeWriteFile file(filePath); + + QStringEncoder encoder = loadOrder + ? QStringEncoder(QStringConverter::Encoding::Utf8) + : QStringEncoder(QStringConverter::Encoding::System); + + file->resize(0); + + file->write( + encoder.encode("# This file was automatically generated by Mod Organizer.\r\n")); + + bool invalidFileNames = false; + int writtenCount = 0; + + QStringList plugins = pluginList->pluginNames(); + std::sort(plugins.begin(), plugins.end(), + [pluginList](const QString& lhs, const QString& rhs) { + return pluginList->priority(lhs) < pluginList->priority(rhs); + }); + + for (const QString& pluginName : plugins) { + if (loadOrder || (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE)) { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } + } + + if (invalidFileNames) { + reportError(QObject::tr("Some of your plugins have invalid names! These " + "plugins can not be loaded by the game. Please see " + "mo_interface.log for a list of affected plugins " + "and rename them.")); + } + + if (writtenCount == 0) { + qWarning("plugin list would be empty, this is almost certainly wrong. Not " + "saving."); + } else { + file->commit(); + } +} + +QStringList GamebryoGamePlugins::readLoadOrderList(MOBase::IPluginList* pluginList, + const QString& filePath) +{ + QStringList pluginNames = organizer()->managedGame()->primaryPlugins(); + + std::set pluginLookup; + for (auto&& name : pluginNames) { + pluginLookup.insert(name.toLower()); + } + + const auto b = MOBase::forEachLineInFile(filePath, [&](QString s) { + if (!pluginLookup.contains(s.toLower())) { + pluginLookup.insert(s); + pluginNames.push_back(std::move(s)); + } + }); + + if (!b) { + return readPluginList(pluginList); + } + + return pluginNames; +} + +QStringList GamebryoGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + QStringList primary = organizer()->managedGame()->primaryPlugins(); + for (const QString& pluginName : primary) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + QStringList plugins = pluginList->pluginNames(); + QStringList pluginsClone(plugins); + // Do not sort the primary plugins. Their load order should be locked as defined in + // "primaryPlugins". + for (const auto& plugin : pluginsClone) { + if (primary.contains(plugin, Qt::CaseInsensitive)) + plugins.removeAll(plugin); + } + + // Always use filetime loadorder to get the actual load order + 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(); + }); + + // Determine plugin active state by the plugins.txt file. + bool pluginsTxtExists = true; + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + pluginsTxtExists = false; + } + ON_BLOCK_EXIT([&]() { + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + pluginsTxtExists = false; + } + + QStringList activePlugins; + QStringList inactivePlugins; + if (pluginsTxtExists) { + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + QStringEncoder encoder(QStringConverter::Encoding::System); + pluginName = encoder.encode(line.trimmed().constData()); + } + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + activePlugins.push_back(pluginName); + } + } + + for (const auto& pluginName : plugins) { + if (!activePlugins.contains(pluginName, Qt::CaseInsensitive)) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + } else { + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return primary + plugins; +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryogameplugins.h b/libs/game_gamebryo/src/gamebryo/gamebryogameplugins.h new file mode 100644 index 0000000..ca70c75 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryogameplugins.h @@ -0,0 +1,38 @@ +#ifndef GAMEBRYOGAMEPLUGINS_H +#define GAMEBRYOGAMEPLUGINS_H + +#include +#include +#include +#include + +class GamebryoGamePlugins : public MOBase::GamePlugins +{ +public: + GamebryoGamePlugins(MOBase::IOrganizer* organizer); + + virtual void writePluginLists(const MOBase::IPluginList* pluginList) override; + virtual void readPluginLists(MOBase::IPluginList* pluginList) override; + virtual QStringList getLoadOrder() override; + +protected: + MOBase::IOrganizer* organizer() const { return m_Organizer; } + + virtual void writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath); + virtual void writeLoadOrderList(const MOBase::IPluginList* pluginList, + const QString& filePath); + virtual QStringList readLoadOrderList(MOBase::IPluginList* pluginList, + const QString& filePath); + virtual QStringList readPluginList(MOBase::IPluginList* pluginList); + +protected: + MOBase::IOrganizer* m_Organizer; + QDateTime m_LastRead; + +private: + void writeList(const MOBase::IPluginList* pluginList, const QString& filePath, + bool loadOrder); +}; + +#endif // GAMEBRYOGAMEPLUGINS_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp b/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp new file mode 100644 index 0000000..cf78eba --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp @@ -0,0 +1,140 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "gamebryolocalsavegames.h" +#include "registry.h" +#include +#include +#include +#include +#include + +#include "gamegamebryo.h" + +GamebryoLocalSavegames::GamebryoLocalSavegames(const GameGamebryo* game, + const QString& iniFileName) + : m_Game{game}, m_IniFileName(iniFileName) +{} + +MappingType GamebryoLocalSavegames::mappings(const QDir& profileSaveDir) const +{ + return {{profileSaveDir.absolutePath(), localSavesDirectory().absolutePath(), true, + true}}; +} + +QString GamebryoLocalSavegames::localSavesDummy() const +{ + return "__MO_Saves\\"; +} + +QDir GamebryoLocalSavegames::localSavesDirectory() const +{ + return QDir(m_Game->myGamesPath()).absoluteFilePath(localSavesDummy()); +} + +QDir GamebryoLocalSavegames::localGameDirectory() const +{ + return QDir(m_Game->myGamesPath()).absolutePath(); +} + +bool GamebryoLocalSavegames::prepareProfile(MOBase::IProfile* profile) +{ + bool enable = profile->localSavesEnabled(); + + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : localGameDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + QString saveIni = profile->absolutePath() + "/" + "savepath.ini"; + + // Get the current sLocalSavePath + WCHAR currentPath[MAX_PATH]; + GetPrivateProfileStringW(L"General", L"sLocalSavePath", L"SKIP_ME", currentPath, + MAX_PATH, iniFilePath.toStdWString().c_str()); + bool alreadyEnabled = + wcscmp(currentPath, localSavesDummy().toStdWString().c_str()) == 0; + + // Get the current bUseMyGamesDirectory + WCHAR currentMyGames[MAX_PATH]; + GetPrivateProfileStringW(L"General", L"bUseMyGamesDirectory", L"SKIP_ME", + currentMyGames, MAX_PATH, + iniFilePath.toStdWString().c_str()); + + // Create the __MO_Saves directory if local saves are enabled and it doesn't exist + if (enable) { + QDir saves = localSavesDirectory(); + if (!saves.exists()) { + saves.mkdir("."); + } + } + + // Set the path to __MO_Saves if it's not already + if (enable && !alreadyEnabled) { + // If the path is not blank, save it to savepath.ini + if (wcscmp(currentPath, L"SKIP_ME") != 0) { + MOBase::WriteRegistryValue(L"General", L"sLocalSavePath", currentPath, + saveIni.toStdWString().c_str()); + } + if (wcscmp(currentMyGames, L"SKIP_ME") != 0) { + MOBase::WriteRegistryValue(L"General", L"bUseMyGamesDirectory", currentMyGames, + saveIni.toStdWString().c_str()); + } + MOBase::WriteRegistryValue(L"General", L"sLocalSavePath", + localSavesDummy().toStdWString().c_str(), + iniFilePath.toStdWString().c_str()); + MOBase::WriteRegistryValue(L"General", L"bUseMyGamesDirectory", L"1", + iniFilePath.toStdWString().c_str()); + } + + // Get rid of the local saves setting if it's still there + if (!enable && alreadyEnabled) { + // If savepath.ini exists, use it and delete it + if (QFile::exists(saveIni)) { + WCHAR savedPath[MAX_PATH]; + WCHAR savedMyGames[MAX_PATH]; + GetPrivateProfileStringW(L"General", L"sLocalSavePath", L"DELETE_ME", savedPath, + MAX_PATH, saveIni.toStdWString().c_str()); + GetPrivateProfileStringW(L"General", L"bUseMyGamesDirectory", L"DELETE_ME", + savedMyGames, MAX_PATH, saveIni.toStdWString().c_str()); + if (wcscmp(savedPath, L"DELETE_ME") != 0) { + MOBase::WriteRegistryValue(L"General", L"sLocalSavePath", savedPath, + iniFilePath.toStdWString().c_str()); + } else { + MOBase::WriteRegistryValue(L"General", L"sLocalSavePath", NULL, + iniFilePath.toStdWString().c_str()); + } + if (wcscmp(savedMyGames, L"DELETE_ME") != 0) { + MOBase::WriteRegistryValue(L"General", L"bUseMyGamesDirectory", savedMyGames, + iniFilePath.toStdWString().c_str()); + } else { + MOBase::WriteRegistryValue(L"General", L"bUseMyGamesDirectory", NULL, + iniFilePath.toStdWString().c_str()); + } + QFile::remove(saveIni); + } + // Otherwise just delete the setting + else { + MOBase::WriteRegistryValue(L"General", L"sLocalSavePath", NULL, + iniFilePath.toStdWString().c_str()); + MOBase::WriteRegistryValue(L"General", L"bUseMyGamesDirectory", NULL, + iniFilePath.toStdWString().c_str()); + } + } + + return enable != alreadyEnabled; +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.h b/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.h new file mode 100644 index 0000000..e5f2a92 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.h @@ -0,0 +1,54 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef GAMEBRYOLOCALSAVEGAMES_H +#define GAMEBRYOLOCALSAVEGAMES_H + +#include + +#include +#include + +class GameGamebryo; + +class GamebryoLocalSavegames : public MOBase::LocalSavegames +{ + +public: + GamebryoLocalSavegames(const GameGamebryo* game, const QString& iniFileName); + + virtual MappingType mappings(const QDir& profileSaveDir) const override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +protected: + // return the path from the local game directory to the local saves folder + // + // this is virtual so game plugins for complete game overhauld (Enderal, Nehrim, etc.) + // can override it properly + // + virtual QString localSavesDummy() const; + + QDir localSavesDirectory() const; + QDir localGameDirectory() const; + +private: + const GameGamebryo* m_Game; + QString m_IniFileName; +}; + +#endif // GAMEBRYOLOCALSAVEGAMES_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.cpp b/libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.cpp new file mode 100644 index 0000000..b5bace6 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.cpp @@ -0,0 +1,76 @@ +#include + +#include "gamebryomoddatachecker.h" + +/** + * @return the list of possible folder names in data. + */ +auto GamebryoModDataChecker::possibleFolderNames() const -> const FileNameSet& +{ + static FileNameSet result{"fonts", + "interface", + "menus", + "meshes", + "music", + "scripts", + "shaders", + "sound", + "strings", + "textures", + "trees", + "video", + "facegen", + "materials", + "skse", + "obse", + "mwse", + "nvse", + "fose", + "f4se", + "distantlod", + "asi", + "SkyProc Patchers", + "Tools", + "MCM", + "icons", + "bookart", + "distantland", + "mits", + "splash", + "dllplugins", + "CalienteTools", + "NetScriptFramework", + "shadersfx"}; + return result; +} + +/** + * @return the extensions of possible files in data. + */ +auto GamebryoModDataChecker::possibleFileExtensions() const -> const FileNameSet& +{ + static FileNameSet result{"esp", "esm", "esl", "bsa", "ba2", "modgroups", "ini"}; + return result; +} + +GamebryoModDataChecker::GamebryoModDataChecker(const GameGamebryo* game) : m_Game(game) +{} + +GamebryoModDataChecker::CheckReturn GamebryoModDataChecker::dataLooksValid( + std::shared_ptr fileTree) const +{ + auto& folders = possibleFolderNames(); + auto& suffixes = possibleFileExtensions(); + for (auto entry : *fileTree) { + if (entry->isDir()) { + if (folders.count(entry->name()) > 0) { + return CheckReturn::VALID; + } + } else { + if (suffixes.count(entry->suffix()) > 0) { + return CheckReturn::VALID; + } + } + } + return CheckReturn::INVALID; +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.h b/libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.h new file mode 100644 index 0000000..e0b0733 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.h @@ -0,0 +1,46 @@ +#ifndef GAMEBRYO_MODATACHECKER_H +#define GAMEBRYO_MODATACHECKER_H + +#include +#include + +class GameGamebryo; + +/** + * @brief ModDataChecker for GameBryo games that look at folder and files in the "data" + * directory. + * + * The default implementation is game-agnostic and uses the list of folders and file + * extensions that were used before the ModDataChecker feature was added. It is possible + * to inherit the class to provide custom list of folders or filenames. + */ +class GamebryoModDataChecker : public MOBase::ModDataChecker +{ +public: + /** + * @brief Construct a new mod-data checker for GameBryo games. + */ + GamebryoModDataChecker(const GameGamebryo* game); + + virtual CheckReturn + dataLooksValid(std::shared_ptr fileTree) const override; + +protected: + GameGamebryo const* const m_Game; + + using FileNameSet = std::set; + + const GameGamebryo* game() const { return m_Game; } + + /** + * @return the list of possible folder names in data. + */ + virtual const FileNameSet& possibleFolderNames() const; + + /** + * @return the extensions of possible files in data. + */ + virtual const FileNameSet& possibleFileExtensions() const; +}; + +#endif // GAMEBRYO_MODATACHECKER_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.cpp b/libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.cpp new file mode 100644 index 0000000..85b66b0 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.cpp @@ -0,0 +1,116 @@ +#include "gamebryomoddatacontent.h" + +#include +#include + +GamebryoModDataContent::GamebryoModDataContent( + MOBase::IGameFeatures const* gameFeatures) + : m_GameFeatures(gameFeatures), m_Enabled(CONTENT_MODGROUP + 1, true) +{} + +std::vector +GamebryoModDataContent::getAllContents() const +{ + static std::vector GAMEBRYO_CONTENTS{ + {CONTENT_PLUGIN, QT_TR_NOOP("Plugins (ESP/ESM/ESL)"), ":/MO/gui/content/plugin"}, + {CONTENT_OPTIONAL, QT_TR_NOOP("Optional Plugins"), "", true}, + {CONTENT_INTERFACE, QT_TR_NOOP("Interface"), ":/MO/gui/content/interface"}, + {CONTENT_MESH, QT_TR_NOOP("Meshes"), ":/MO/gui/content/mesh"}, + {CONTENT_BSA, QT_TR_NOOP("Bethesda Archive"), ":/MO/gui/content/bsa"}, + {CONTENT_SCRIPT, QT_TR_NOOP("Scripts (Papyrus)"), ":/MO/gui/content/script"}, + {CONTENT_SKSE, QT_TR_NOOP("Script Extender Plugin"), ":/MO/gui/content/skse"}, + {CONTENT_SKSE_FILES, QT_TR_NOOP("Script Extender Files"), "", true}, + {CONTENT_SKYPROC, QT_TR_NOOP("SkyProc Patcher"), ":/MO/gui/content/skyproc"}, + {CONTENT_SOUND, QT_TR_NOOP("Sound or Music"), ":/MO/gui/content/sound"}, + {CONTENT_TEXTURE, QT_TR_NOOP("Textures"), ":/MO/gui/content/texture"}, + {CONTENT_MCM, QT_TR_NOOP("MCM Configuration"), ":/MO/gui/content/menu"}, + {CONTENT_INI, QT_TR_NOOP("INI Files"), ":/MO/gui/content/inifile"}, + {CONTENT_FACEGEN, QT_TR_NOOP("FaceGen Data"), ":/MO/gui/content/facegen"}, + {CONTENT_MODGROUP, QT_TR_NOOP("ModGroup Files"), ":/MO/gui/content/modgroup"}}; + + // Copy the list of enabled contents: + std::vector contents; + std::copy_if(std::begin(GAMEBRYO_CONTENTS), std::end(GAMEBRYO_CONTENTS), + std::back_inserter(contents), [this](auto e) { + return m_Enabled[e.id()]; + }); + return contents; +} + +std::vector GamebryoModDataContent::getContentsFor( + std::shared_ptr fileTree) const +{ + std::vector contents; + + for (auto e : *fileTree) { + if (e->isFile()) { + auto suffix = e->suffix().toLower(); + if (m_Enabled[CONTENT_PLUGIN] && + (suffix == "esp" || suffix == "esm" || suffix == "esl")) { + contents.push_back(CONTENT_PLUGIN); + } else if (m_Enabled[CONTENT_BSA] && (suffix == "bsa" || suffix == "ba2")) { + contents.push_back(CONTENT_BSA); + } else if (m_Enabled[CONTENT_INI] && suffix == "ini" && + e->compare("meta.ini") != 0) { + contents.push_back(CONTENT_INI); + } else if (m_Enabled[CONTENT_MODGROUP] && suffix == "modgroups") { + contents.push_back(CONTENT_MODGROUP); + } + } else { + if (m_Enabled[CONTENT_TEXTURE] && + (e->compare("textures") == 0 || e->compare("icons") == 0 || + e->compare("bookart") == 0)) { + contents.push_back(CONTENT_TEXTURE); + } else if (m_Enabled[CONTENT_MESH] && e->compare("meshes") == 0) { + contents.push_back(CONTENT_MESH); + } else if (m_Enabled[CONTENT_INTERFACE] && + (e->compare("interface") == 0 || e->compare("menus") == 0)) { + contents.push_back(CONTENT_INTERFACE); + } else if (m_Enabled[CONTENT_SOUND] && e->compare("music") == 0 || + e->compare("sound") == 0) { + contents.push_back(CONTENT_SOUND); + } else if (m_Enabled[CONTENT_SCRIPT] && e->compare("scripts") == 0) { + contents.push_back(CONTENT_SCRIPT); + } else if (m_Enabled[CONTENT_SKYPROC] && e->compare("SkyProc Patchers") == 0) { + contents.push_back(CONTENT_SKYPROC); + } else if (m_Enabled[CONTENT_MCM] && e->compare("MCM") == 0) { + contents.push_back(CONTENT_MCM); + } else if (m_Enabled[CONTENT_OPTIONAL] && e->compare("Optional") == 0 && + e->astree()->size() > 0) { + contents.push_back(CONTENT_OPTIONAL); + } + } + } + + if (m_Enabled[CONTENT_FACEGEN]) { + auto e1 = fileTree->findDirectory("meshes/actors/character/facegendata"); + if (e1) { + contents.push_back(CONTENT_FACEGEN); + } else { + auto e2 = fileTree->findDirectory("textures/actors/character/facegendata"); + if (e2) { + contents.push_back(CONTENT_FACEGEN); + } + } + } + + auto extender = m_GameFeatures->gameFeature(); + if (extender != nullptr) { + auto e = fileTree->findDirectory(extender->PluginPath()); + if (e) { + if (m_Enabled[CONTENT_SKSE_FILES]) { + contents.push_back(CONTENT_SKSE_FILES); + } + if (m_Enabled[CONTENT_SKSE]) { + for (auto f : *e) { + if (f->hasSuffix("dll")) { + contents.push_back(CONTENT_SKSE); + break; + } + } + } + } + } + + return contents; +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.h b/libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.h new file mode 100644 index 0000000..be02331 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.h @@ -0,0 +1,75 @@ +#ifndef GAMEBRYO_MODDATACONTENT_H +#define GAMEBRYO_MODDATACONTENT_H + +#include +#include + +namespace MOBase +{ +class IGameFeatures; +} + +/** + * @brief ModDataContent for GameBryo games. + * + */ +class GamebryoModDataContent : public MOBase::ModDataContent +{ +protected: + /** + * Note: These are used to index m_Enabled so should have standard + * enum values, not custom ones. + */ + enum EContent + { + CONTENT_PLUGIN, + CONTENT_OPTIONAL, + CONTENT_TEXTURE, + CONTENT_MESH, + CONTENT_BSA, + CONTENT_INTERFACE, + CONTENT_SOUND, + CONTENT_SCRIPT, + CONTENT_SKSE, + CONTENT_SKSE_FILES, + CONTENT_SKYPROC, + CONTENT_MCM, + CONTENT_INI, + CONTENT_FACEGEN, + CONTENT_MODGROUP + }; + + /** + * This is the first value that can be used for game-specific contents. + */ + constexpr static auto CONTENT_NEXT_VALUE = CONTENT_MODGROUP + 1; + +public: + /** + * + */ + GamebryoModDataContent(const MOBase::IGameFeatures* gameFeatures); + + /** + * @return the list of all possible contents for the corresponding game. + */ + virtual std::vector getAllContents() const override; + + /** + * @brief Retrieve the list of contents in the given tree. + * + * @param fileTree The tree corresponding to the mod to retrieve contents for. + * + * @return the IDs of the content in the given tree. + */ + virtual std::vector + getContentsFor(std::shared_ptr fileTree) const override; + +protected: + MOBase::IGameFeatures const* const m_GameFeatures; + + // List of enabled contents: + std::vector m_Enabled; +}; + +#endif // GAMEBRYO_MODDATACONTENT_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegame.cpp b/libs/game_gamebryo/src/gamebryo/gamebryosavegame.cpp new file mode 100644 index 0000000..596b19b --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegame.cpp @@ -0,0 +1,606 @@ +#include "gamebryosavegame.h" + +#include "iplugingame.h" +#include "log.h" +#include "scriptextender.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "gamegamebryo.h" +#include "imoinfo.h" + +#define CHUNK 16384 + +GamebryoSaveGame::GamebryoSaveGame(QString const& file, GameGamebryo const* game, + bool const lightEnabled, bool const mediumEnabled) + : m_FileName(file), m_CreationTime(QFileInfo(file).lastModified()), m_Game(game), + m_MediumEnabled(mediumEnabled), m_LightEnabled(lightEnabled), + m_DataFields([this]() { + return fetchDataFields(); + }) +{} + +GamebryoSaveGame::~GamebryoSaveGame() {} + +QString GamebryoSaveGame::getFilepath() const +{ + return m_FileName; +} + +QDateTime GamebryoSaveGame::getCreationTime() const +{ + return m_CreationTime; +} + +QString GamebryoSaveGame::getName() const +{ + return QObject::tr("%1, #%2, Level %3, %4") + .arg(m_PCName) + .arg(m_SaveNumber) + .arg(m_PCLevel) + .arg(m_PCLocation); +} + +QString GamebryoSaveGame::getSaveGroupIdentifier() const +{ + return m_PCName; +} + +QStringList GamebryoSaveGame::allFiles() const +{ + // This returns all valid files associated with this game + QStringList res = {m_FileName}; + auto e = m_Game->m_Organizer->gameFeatures()->gameFeature(); + if (e != nullptr) { + QFileInfo file(m_FileName); + QFileInfo SEfile(file.absolutePath() + "/" + file.completeBaseName() + "." + + m_Game->savegameSEExtension()); + if (SEfile.exists()) { + res.push_back(SEfile.absoluteFilePath()); + } + } + return res; +} + +bool GamebryoSaveGame::hasScriptExtenderFile() const +{ + QFileInfo file(m_FileName); + QFileInfo SEfile(file.absolutePath() + "/" + file.completeBaseName() + "." + + m_Game->savegameSEExtension()); + return SEfile.exists(); +} + +void GamebryoSaveGame::setCreationTime(_SYSTEMTIME const& ctime) +{ + QDate date; + date.setDate(ctime.wYear, ctime.wMonth, ctime.wDay); + QTime time; + time.setHMS(ctime.wHour, ctime.wMinute, ctime.wSecond, ctime.wMilliseconds); + + m_CreationTime = QDateTime(date, time, Qt::UTC); +} + +GamebryoSaveGame::FileWrapper::FileWrapper(QString const& filepath, + QString const& expected) + : m_File(filepath), m_HasFieldMarkers(false), + m_PluginString(StringType::TYPE_WSTRING), + m_PluginStringFormat(StringFormat::UTF8), m_NextChunk(0) +{ + if (!m_File.open(QIODevice::ReadOnly)) { + throw std::runtime_error( + QObject::tr("failed to open %1").arg(filepath).toUtf8().constData()); + } + + std::vector fileID(expected.length() + 1); + m_File.read(fileID.data(), expected.length()); + fileID[expected.length()] = '\0'; + + QString id(fileID.data()); + if (expected != id) { + throw std::runtime_error( + QObject::tr("wrong file format - expected %1 got \'%2\' for %3") + .arg(expected) + .arg(id) + .arg(filepath) + .toUtf8() + .constData()); + } +} + +void GamebryoSaveGame::FileWrapper::setHasFieldMarkers(bool state) +{ + m_HasFieldMarkers = state; +} + +void GamebryoSaveGame::FileWrapper::setPluginString(StringType type) +{ + m_PluginString = type; +} + +void GamebryoSaveGame::FileWrapper::setPluginStringFormat(StringFormat type) +{ + m_PluginStringFormat = type; +} + +void GamebryoSaveGame::FileWrapper::readQDataStream(QDataStream& data, void* buff, + std::size_t length) +{ + int read = data.readRawData(static_cast(buff), static_cast(length)); + bool result = true; + if (read != length && m_CompressionType == 1) { + bool result = readNextChunk(); + if (result) { + read += data.readRawData(static_cast(buff) + read, + static_cast(length - read)); + } + } + if (read != length || !result) { + throw std::runtime_error("unexpected end of file"); + } +} + +template +void GamebryoSaveGame::FileWrapper::readQDataStream(QDataStream& data, T& value) +{ + static_assert(std::is_trivial_v && std::is_standard_layout_v); + readQDataStream(data, &value, sizeof(T)); +} + +void GamebryoSaveGame::FileWrapper::skipQDataStream(QDataStream& data, + std::size_t length) +{ + int skip = data.skipRawData(static_cast(length)); + bool result = true; + if (skip != length && m_CompressionType == 1) { + result = readNextChunk(); + if (result) { + skip += data.skipRawData(static_cast(length - skip)); + } + } + if (skip != length || !result) { + throw std::runtime_error("unexpected end of file"); + } +} + +template <> +void GamebryoSaveGame::FileWrapper::read(QString& value) +{ + if (m_CompressionType == 0) { + unsigned short length; + if (m_PluginString == StringType::TYPE_BSTRING || + m_PluginString == StringType::TYPE_BZSTRING) { + unsigned char len; + read(len); + length = m_PluginString == StringType::TYPE_BZSTRING ? len + 1 : len; + } else { + read(length); + } + + if (m_HasFieldMarkers) { + skip(); + } + + QByteArray buffer; + buffer.resize(length); + + read(buffer.data(), + m_PluginString == StringType::TYPE_BZSTRING ? length - 1 : length); + + if (m_PluginString == StringType::TYPE_BZSTRING) + buffer[length - 1] = '\0'; + + if (m_HasFieldMarkers) { + skip(); + } + + if (m_PluginStringFormat == StringFormat::UTF8) + value = QString::fromUtf8(buffer.constData()); + else + value = QString::fromLocal8Bit(buffer.constData()); + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + unsigned short length; + if (m_PluginString == StringType::TYPE_BSTRING || + m_PluginString == StringType::TYPE_BZSTRING) { + unsigned char len; + readQDataStream(*m_Data, len); + length = m_PluginString == StringType::TYPE_BZSTRING ? len + 1 : len; + } else { + readQDataStream(*m_Data, length); + } + + if (m_HasFieldMarkers) { + skip(); + } + + QByteArray buffer; + buffer.resize(length); + + readQDataStream(*m_Data, buffer.data(), + m_PluginString == StringType::TYPE_BZSTRING ? length - 1 : length); + + if (m_PluginString == StringType::TYPE_BZSTRING) + buffer[length - 1] = '\0'; + + if (m_HasFieldMarkers) { + m_Data->skipRawData(1); + } + + if (m_PluginStringFormat == StringFormat::UTF8) + value = QString::fromUtf8(buffer.constData()); + else + value = QString::fromLocal8Bit(buffer.constData()); + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + } +} + +void GamebryoSaveGame::FileWrapper::read(void* buff, std::size_t length) +{ + int read = m_File.read(static_cast(buff), length); + if (read != length) { + throw std::runtime_error("unexpected end of file"); + } +} + +QImage GamebryoSaveGame::FileWrapper::readImage(int scale, bool alpha) +{ + unsigned long width; + read(width); + unsigned long height; + read(height); + return readImage(width, height, scale, alpha); +} + +QImage GamebryoSaveGame::FileWrapper::readImage(unsigned long width, + unsigned long height, int scale, + bool alpha) +{ + int bpp = alpha ? 4 : 3; + QScopedArrayPointer buffer(new unsigned char[width * height * bpp]); + read(buffer.data(), width * height * bpp); + QImage image(buffer.data(), width, height, + alpha ? QImage::Format_RGBA8888_Premultiplied : QImage::Format_RGB888); + + // We need to copy the image here because QImage does not make a copy of the + // buffer when constructed. + if (scale != 0) { + return image.copy().scaledToWidth(scale); + } else { + return image.copy(); + } +} + +void GamebryoSaveGame::FileWrapper::setCompressionType(uint16_t compressionType) +{ + m_CompressionType = compressionType; +} + +void GamebryoSaveGame::FileWrapper::closeCompressedData() +{ + if (m_CompressionType == 0) { + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + m_NextChunk = 0; + m_UncompressedSize = 0; + m_Data->device()->close(); + delete m_Data; + } else + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); +} + +bool GamebryoSaveGame::FileWrapper::openCompressedData(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + return false; + } else if (m_CompressionType == 1) { + read(m_NextChunk); + read(m_UncompressedSize); + QByteArray placeholder; + m_Data = new QDataStream(placeholder); + bool result = readNextChunk(); + if (result) + skipQDataStream(*m_Data, bytesToIgnore); + return result; + } else if (m_CompressionType == 2) { + uint32_t uncompressedSize; + read(uncompressedSize); + uint32_t compressedSize; + read(compressedSize); + QByteArray compressed; + compressed.resize(compressedSize); + read(compressed.data(), compressedSize); + QByteArray decompressed; + decompressed.resize(uncompressedSize); + LZ4_decompress_safe_partial(compressed.data(), decompressed.data(), compressedSize, + uncompressedSize, uncompressedSize); + compressed.clear(); + + m_Data = new QDataStream(decompressed); + skipQDataStream(*m_Data, bytesToIgnore); + + return true; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return false; + } +} + +bool GamebryoSaveGame::FileWrapper::readNextChunk() +{ + uint32_t have; + uint64_t read = 0; + std::unique_ptr inBuffer = std::make_unique(CHUNK); + std::unique_ptr outBuffer = std::make_unique(CHUNK); + QByteArray finalData; + m_Data->device()->close(); + delete m_Data; + z_stream stream{}; + try { + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = 0; + stream.next_in = Z_NULL; + if (m_NextChunk >= m_File.size() || finalData.size() == m_UncompressedSize) + return false; + m_File.seek(m_NextChunk); + int zlibRet = inflateInit2(&stream, 15 + 32); + if (zlibRet != Z_OK) { + return false; + } + do { + stream.avail_in = m_File.read(inBuffer.get(), CHUNK); + read += stream.avail_in; + if (!m_File.isReadable()) { + (void)inflateEnd(&stream); + return false; + } + if (stream.avail_in == 0) + break; + stream.next_in = reinterpret_cast(inBuffer.get()); + do { + stream.avail_out = CHUNK; + stream.next_out = reinterpret_cast(outBuffer.get()); + zlibRet = inflate(&stream, Z_NO_FLUSH); + if ((zlibRet != Z_OK) && (zlibRet != Z_STREAM_END) && + (zlibRet != Z_BUF_ERROR)) { + return false; + } + have = CHUNK - stream.avail_out; + finalData += QByteArray::fromRawData(outBuffer.get(), have); + } while (stream.avail_out == 0); + read -= stream.avail_in; + } while (zlibRet != Z_STREAM_END); + inflateEnd(&stream); + uint64_t remainder = (m_NextChunk + read) % 16; + uint64_t next = m_NextChunk + read + 16 - (remainder == 0 ? 16 : remainder); + m_NextChunk = next; + } catch (const std::exception&) { + inflateEnd(&stream); + return false; + } + m_Data = new QDataStream(finalData); + return true; +} + +uint8_t GamebryoSaveGame::FileWrapper::readChar(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint8_t version; + read(version); + return version; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint8_t version; + readQDataStream(*m_Data, version); + return version; + + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +uint16_t GamebryoSaveGame::FileWrapper::readShort(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint16_t size; + read(size); + return size; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint16_t size; + readQDataStream(*m_Data, size); + return size; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +uint32_t GamebryoSaveGame::FileWrapper::readInt(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint32_t size; + read(size); + return size; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint32_t size; + readQDataStream(*m_Data, size); + return size; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +uint64_t GamebryoSaveGame::FileWrapper::readLong(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint64_t size; + read(size); + return size; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint64_t size; + readQDataStream(*m_Data, size); + return size; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +float_t GamebryoSaveGame::FileWrapper::readFloat(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + float_t value; + read(value); + return value; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + float_t value; + readQDataStream(*m_Data, value); + return value; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +QStringList GamebryoSaveGame::FileWrapper::readPlugins(int bytesToIgnore, int extraData, + const QStringList& corePlugins) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint8_t count; + read(count); + return readPluginData(count, extraData, corePlugins); + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + skipQDataStream(*m_Data, bytesToIgnore); + uint8_t count; + readQDataStream(*m_Data, count); + return readPluginData(count, extraData, corePlugins); + } + return {}; +} + +QStringList +GamebryoSaveGame::FileWrapper::readLightPlugins(int bytesToIgnore, int extraData, + const QStringList& corePlugins) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint16_t count; + read(count); + return readPluginData(count, extraData, corePlugins); + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + skipQDataStream(*m_Data, bytesToIgnore); + uint16_t count; + readQDataStream(*m_Data, count); + return readPluginData(count, extraData, corePlugins); + } + return {}; +} + +QStringList +GamebryoSaveGame::FileWrapper::readMediumPlugins(int bytesToIgnore, int extraData, + const QStringList& corePlugins) +{ + if (m_CompressionType != 1) { + return {}; + } else { + skipQDataStream(*m_Data, bytesToIgnore); + uint32_t count; + readQDataStream(*m_Data, count); + return readPluginData(count, extraData, corePlugins); + } +} + +QStringList GamebryoSaveGame::FileWrapper::readPluginData(uint32_t count, int extraData, + const QStringList corePlugins) +{ + QStringList plugins; + plugins.reserve(count); + if (m_CompressionType == 0) { + for (std::size_t i = 0; i < count; ++i) { + QString name; + read(name); + plugins.push_back(name); + } + } else { + for (std::size_t i = 0; i < count; ++i) { + QString name; + read(name); + plugins.push_back(name); + bool isCustomPlugin; + if (extraData) { + if (extraData > 1) { + readQDataStream(*m_Data, isCustomPlugin); + } else { + isCustomPlugin = !corePlugins.contains(name); + } + if (isCustomPlugin) { + QString creationName; + QString creationId; + uint16_t flagsSize; + uint8_t isCreation; + read(creationName); + read(creationId); + readQDataStream(*m_Data, flagsSize); + skipQDataStream(*m_Data, flagsSize); + readQDataStream(*m_Data, isCreation); + } + } + } + } + return plugins; +} + +void GamebryoSaveGame::FileWrapper::close() +{ + m_File.close(); +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegame.h b/libs/game_gamebryo/src/gamebryo/gamebryosavegame.h new file mode 100644 index 0000000..44d0584 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegame.h @@ -0,0 +1,240 @@ +#ifndef GAMEBRYOSAVEGAME_H +#define GAMEBRYOSAVEGAME_H + +#include "isavegame.h" +#include "memoizedlock.h" + +#include +#include +#include +#include +#include + +#include +#include + +struct _SYSTEMTIME; + +namespace MOBase +{ +class IPluginGame; +} // namespace MOBase + +class GameGamebryo; + +class GamebryoSaveGame : public MOBase::ISaveGame +{ +public: + GamebryoSaveGame(QString const& file, GameGamebryo const* game, + bool const lightEnabled = false, bool const mediumEnabled = false); + + virtual ~GamebryoSaveGame(); + +public: // ISaveGame interface + virtual QString getFilepath() const override; + virtual QDateTime getCreationTime() const override; + virtual QString getName() const override; + virtual QString getSaveGroupIdentifier() const override; + virtual QStringList allFiles() const override; + +public: + bool hasScriptExtenderFile() const; + + // Simple getters + virtual QString getPCName() const { return m_PCName; } + virtual unsigned short getPCLevel() const { return m_PCLevel; } + virtual QString getPCLocation() const { return m_PCLocation; } + virtual unsigned long getSaveNumber() const { return m_SaveNumber; } + + QStringList const& getPlugins() const { return m_DataFields.value()->Plugins; } + QStringList const& getMediumPlugins() const + { + return m_DataFields.value()->MediumPlugins; + } + QStringList const& getLightPlugins() const + { + return m_DataFields.value()->LightPlugins; + } + QImage const& getScreenshot() const { return m_DataFields.value()->Screenshot; } + + bool isMediumEnabled() const { return m_MediumEnabled; } + + bool isLightEnabled() const { return m_LightEnabled; } + + enum class StringType + { + TYPE_BZSTRING, + TYPE_BSTRING, + TYPE_WSTRING + }; + + enum class StringFormat + { + UTF8, + LOCAL8BIT + }; + +protected: + friend class FileWrapper; + + class FileWrapper + { + public: + /** + * @brief Construct the save file information. + * + * @param filepath The path to the save file. + * @params expected Expecte bytes at start of file. + * + **/ + FileWrapper(QString const& filepath, QString const& expected); + + /** Set this for save games that have a marker at the end of each + * field. Specifically fallout + **/ + void setHasFieldMarkers(bool); + + /** Set bz string mode (1 byte length, null terminated) + **/ + void setPluginString(StringType); + + /** Set string format (utf-8, windows local 8 bit strings) + **/ + void setPluginStringFormat(StringFormat); + + template + void skip(int count = 1) + { + if (!m_File.seek(m_File.pos() + count * sizeof(T))) { + throw std::runtime_error("unexpected end of file"); + } + } + + template + void read(T& value) + { + int read = m_File.read(reinterpret_cast(&value), sizeof(T)); + if (read != sizeof(T)) { + throw std::runtime_error("unexpected end of file"); + } + if (m_HasFieldMarkers) { + skip(); + } + } + + template <> + void read(QString& value); + + void seek(unsigned long pos) + { + if (!m_File.seek(pos)) { + throw std::runtime_error("unexpected end of file"); + } + } + + void read(void* buff, std::size_t length); + + /* Reads RGB image from save + * Assumes picture dimentions come immediately before the save + */ + QImage readImage(int scale = 0, bool alpha = false); + + /* Reads RGB image from save */ + QImage readImage(unsigned long width, unsigned long height, int scale = 0, + bool alpha = false); + + /* Sets the compression type. */ + void setCompressionType(uint16_t type); + + /* uncompress the begining of the compressed block */ + bool openCompressedData(int bytesToIgnore = 0); + + /* read the next compressed block */ + bool readNextChunk(); + + /* frees the uncompressed block */ + void closeCompressedData(); + + /* Read the save game version in the compressed block */ + uint8_t readChar(int bytesToIgnore = 0); + + uint16_t readShort(int bytesToIgnore = 0); + + uint32_t readInt(int bytesToIgnore = 0); + + uint64_t readLong(int bytesToIgnore = 0); + + float_t readFloat(int bytesToIgnore = 0); + + /* Read the plugin list */ + QStringList readPlugins(int bytesToIgnore = 0, int extraData = 0, + const QStringList& corePlugins = {}); + + /* Read the light plugin list */ + QStringList readLightPlugins(int bytesToIgnore = 0, int extraData = 0, + const QStringList& corePlugins = {}); + + /* Read the medium plugin list */ + QStringList readMediumPlugins(int bytesToIgnore = 0, int extraData = 0, + const QStringList& corePlugins = {}); + + void close(); + + private: + QFile m_File; + uint64_t m_NextChunk; + uint64_t m_UncompressedSize; + bool m_HasFieldMarkers; + StringType m_PluginString; + StringFormat m_PluginStringFormat; + QDataStream* m_Data; + uint16_t m_CompressionType = 0; + + private: + template + void readQDataStream(QDataStream& data, T& value); + + void readQDataStream(QDataStream& data, void* buff, std::size_t length); + + void skipQDataStream(QDataStream& data, std::size_t length); + + QStringList readPluginData(uint32_t count, int extraData, + const QStringList corePlugins); + }; + + void setCreationTime(_SYSTEMTIME const& time); + + GameGamebryo const* m_Game; + bool m_MediumEnabled; + bool m_LightEnabled; + + QString m_FileName; + QString m_PCName; + unsigned short m_PCLevel; + QString m_PCLocation; + unsigned long m_SaveNumber; + QDateTime m_CreationTime; + + // Those three fields are usually much slower to fetch than + // the other, so we do not fetch them if not needed. + // + // This is virtual so child class can add fields if those are + // hard to access. + struct DataFields + { + QStringList Plugins; + QStringList LightPlugins; + QStringList MediumPlugins; + QImage Screenshot; + + // We need this constructor. + DataFields() {} + virtual ~DataFields() {} + }; + MOBase::MemoizedLocked> m_DataFields; + + // Fetch the field. + virtual std::unique_ptr fetchDataFields() const = 0; +}; + +#endif // GAMEBRYOSAVEGAME_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.cpp b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.cpp new file mode 100644 index 0000000..6d1289a --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.cpp @@ -0,0 +1,100 @@ +#include "gamebryosavegameinfo.h" + +#include "gamebryosavegame.h" +#include "gamebryosavegameinfowidget.h" +#include "gamegamebryo.h" +#include "imodinterface.h" +#include "imoinfo.h" +#include "iplugingame.h" +#include "ipluginlist.h" + +#include +#include +#include + +GamebryoSaveGameInfo::GamebryoSaveGameInfo(GameGamebryo const* game) : m_Game(game) {} + +GamebryoSaveGameInfo::~GamebryoSaveGameInfo() {} + +GamebryoSaveGameInfo::MissingAssets +GamebryoSaveGameInfo::getMissingAssets(MOBase::ISaveGame const& save) const +{ + GamebryoSaveGame const& gamebryoSave = dynamic_cast(save); + MOBase::IOrganizer* organizerCore = m_Game->m_Organizer; + + // collect the list of missing plugins + MissingAssets missingAssets; + + for (QString const& pluginName : gamebryoSave.getPlugins()) { + switch (organizerCore->pluginList()->state(pluginName)) { + case MOBase::IPluginList::STATE_INACTIVE: + missingAssets[pluginName] = + ProvidingModules{organizerCore->pluginList()->origin(pluginName)}; + break; + case MOBase::IPluginList::STATE_MISSING: + missingAssets[pluginName] = ProvidingModules(); + break; + } + } + + for (QString const& pluginName : gamebryoSave.getLightPlugins()) { + switch (organizerCore->pluginList()->state(pluginName)) { + case MOBase::IPluginList::STATE_INACTIVE: + missingAssets[pluginName] = + ProvidingModules{organizerCore->pluginList()->origin(pluginName)}; + break; + case MOBase::IPluginList::STATE_MISSING: + missingAssets[pluginName] = ProvidingModules(); + break; + } + } + + // Find out any other mods that might contain the esp/esm + QStringList espFilter({"*.esp", "*.esl", "*.esm"}); + + QString dataDir(organizerCore->managedGame()->dataDirectory().absolutePath()); + + // Search normal mods. A note: This will also find mods in data. + for (QString const& mod : organizerCore->modList()->allModsByProfilePriority()) { + MOBase::IModInterface* modInfo = organizerCore->modList()->getMod(mod); + QStringList esps = QDir(modInfo->absolutePath()).entryList(espFilter); + for (QString const& esp : esps) { + MissingAssets::iterator iter = missingAssets.find(esp); + if (modInfo->absolutePath() == dataDir) { + // We have to prune esps that reside in the data directory, otherwise + // you get all the unmanaged mods listed as potential candidates for + // enabling + if (modInfo->name() != organizerCore->pluginList()->origin(esp)) { + continue; + } + } + if (iter != missingAssets.end()) { + if (!iter->contains(modInfo->name())) { + iter->push_back(modInfo->name()); + } + } + } + } + + // search in overwrite + { + QDir overwriteDir(organizerCore->overwritePath()); + QStringList esps = overwriteDir.entryList(espFilter); + for (const QString& esp : esps) { + MissingAssets::iterator iter = missingAssets.find(esp); + if (iter != missingAssets.end()) { + if (!iter->contains("")) { + iter->push_back(""); + } + } + } + } + + return missingAssets; +} + +MOBase::ISaveGameInfoWidget* +GamebryoSaveGameInfo::getSaveGameWidget(QWidget* parent) const +{ + return new GamebryoSaveGameInfoWidget(this, parent); +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.h b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.h new file mode 100644 index 0000000..e214293 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.h @@ -0,0 +1,23 @@ +#ifndef GAMEBRYOSAVEGAMEINFO_H +#define GAMEBRYOSAVEGAMEINFO_H + +#include "savegameinfo.h" + +class GameGamebryo; + +class GamebryoSaveGameInfo : public MOBase::SaveGameInfo +{ +public: + GamebryoSaveGameInfo(GameGamebryo const* game); + ~GamebryoSaveGameInfo(); + + virtual MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const override; + + virtual MOBase::ISaveGameInfoWidget* getSaveGameWidget(QWidget*) const override; + +protected: + friend class GamebryoSaveGameInfoWidget; + GameGamebryo const* m_Game; +}; + +#endif // GAMEBRYOSAVEGAMEINFO_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.cpp b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.cpp new file mode 100644 index 0000000..8c95dad --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.cpp @@ -0,0 +1,199 @@ +#include "gamebryosavegameinfowidget.h" +#include "ui_gamebryosavegameinfowidget.h" + +#include "gamebryosavegame.h" +#include "gamebryosavegameinfo.h" +#include "gamegamebryo.h" +#include "imoinfo.h" +#include "ipluginlist.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +GamebryoSaveGameInfoWidget::GamebryoSaveGameInfoWidget(GamebryoSaveGameInfo const* info, + QWidget* parent) + : MOBase::ISaveGameInfoWidget(parent), ui(new Ui::GamebryoSaveGameInfoWidget), + m_Info(info) +{ + ui->setupUi(this); + this->setWindowFlags(Qt::ToolTip | Qt::BypassGraphicsProxyWidget); + setWindowOpacity(style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, 0, this) / + qreal(255.0)); + ui->gameFrame->setStyleSheet("background-color: transparent;"); + + QVBoxLayout* gameLayout = new QVBoxLayout(); + gameLayout->setContentsMargins(0, 0, 0, 0); + gameLayout->setSpacing(2); + ui->gameFrame->setLayout(gameLayout); +} + +GamebryoSaveGameInfoWidget::~GamebryoSaveGameInfoWidget() +{ + delete ui; +} + +void GamebryoSaveGameInfoWidget::setSave(MOBase::ISaveGame const& save) +{ + auto& gamebryoSave = dynamic_cast(save); + ui->saveNumLabel->setText(QString("%1").arg(gamebryoSave.getSaveNumber())); + ui->characterLabel->setText(gamebryoSave.getPCName()); + ui->locationLabel->setText(gamebryoSave.getPCLocation()); + ui->levelLabel->setText(QString("%1").arg(gamebryoSave.getPCLevel())); + // This somewhat contorted code is because on my system at least, the + // old way of doing this appears to give short date and long time. + QDateTime t = gamebryoSave.getCreationTime().toLocalTime(); + ui->dateLabel->setText( + QLocale::system().toString(t.date(), QLocale::FormatType::ShortFormat) + " " + + QLocale::system().toString(t.time())); + ui->screenshotLabel->setPixmap(QPixmap::fromImage(gamebryoSave.getScreenshot())); + if (ui->gameFrame->layout() != nullptr) { + QLayoutItem* item = nullptr; + while ((item = ui->gameFrame->layout()->takeAt(0)) != nullptr) { + delete item->widget(); + delete item; + } + ui->gameFrame->layout()->setSizeConstraint(QLayout::SetFixedSize); + } + + // Resize box to new content + this->resize(0, 0); + + QLayout* layout = ui->gameFrame->layout(); + if (gamebryoSave.hasScriptExtenderFile()) { + QLabel* scriptExtender = new QLabel(tr("Has Script Extender Data")); + QFont headerFont = scriptExtender->font(); + headerFont.setBold(true); + layout->addWidget(scriptExtender); + } + QLabel* header = new QLabel(tr("Missing ESPs")); + QFont headerFont = header->font(); + QFont contentFont = headerFont; + headerFont.setItalic(true); + contentFont.setBold(true); + contentFont.setPointSize(7); + header->setFont(headerFont); + layout->addWidget(header); + int count = 0; + MOBase::IPluginList* pluginList = m_Info->m_Game->m_Organizer->pluginList(); + for (QString const& pluginName : gamebryoSave.getPlugins()) { + if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) { + continue; + } + + ++count; + + if (count > 7) { + break; + } + + QLabel* pluginLabel = new QLabel(pluginName); + pluginLabel->setIndent(10); + pluginLabel->setFont(contentFont); + layout->addWidget(pluginLabel); + } + if (count > 7) { + QLabel* dotDotLabel = new QLabel("..."); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (count == 0) { + QLabel* dotDotLabel = new QLabel(tr("None")); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (gamebryoSave.isMediumEnabled()) { + QLabel* headerEsh = new QLabel(tr("Missing ESHs")); + QFont headerEshFont = headerEsh->font(); + QFont contentEshFont = headerEshFont; + headerEshFont.setItalic(true); + contentEshFont.setBold(true); + contentEshFont.setPointSize(7); + headerEsh->setFont(headerEshFont); + layout->addWidget(headerEsh); + int countEsh = 0; + for (QString const& pluginName : gamebryoSave.getMediumPlugins()) { + if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) { + continue; + } + + ++countEsh; + + if (countEsh > 7) { + break; + } + + QLabel* pluginLabel = new QLabel(pluginName); + pluginLabel->setIndent(10); + pluginLabel->setFont(contentFont); + layout->addWidget(pluginLabel); + } + if (countEsh > 7) { + QLabel* dotDotLabel = new QLabel("..."); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (countEsh == 0) { + QLabel* dotDotLabel = new QLabel(tr("None")); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + } + if (gamebryoSave.isLightEnabled()) { + QLabel* headerEsl = new QLabel(tr("Missing ESLs")); + QFont headerEslFont = headerEsl->font(); + QFont contentEslFont = headerEslFont; + headerEslFont.setItalic(true); + contentEslFont.setBold(true); + contentEslFont.setPointSize(7); + headerEsl->setFont(headerEslFont); + layout->addWidget(headerEsl); + int countEsl = 0; + for (QString const& pluginName : gamebryoSave.getLightPlugins()) { + if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) { + continue; + } + + ++countEsl; + + if (countEsl > 7) { + break; + } + + QLabel* pluginLabel = new QLabel(pluginName); + pluginLabel->setIndent(10); + pluginLabel->setFont(contentFont); + layout->addWidget(pluginLabel); + } + if (countEsl > 7) { + QLabel* dotDotLabel = new QLabel("..."); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (countEsl == 0) { + QLabel* dotDotLabel = new QLabel(tr("None")); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + } +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.h b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.h new file mode 100644 index 0000000..6642863 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.h @@ -0,0 +1,30 @@ +#ifndef GAMEBRYOSAVEGAMEINFOWIDGET_H +#define GAMEBRYOSAVEGAMEINFOWIDGET_H + +#include "isavegameinfowidget.h" + +#include + +class GamebryoSaveGameInfo; + +namespace Ui +{ +class GamebryoSaveGameInfoWidget; +} + +class GamebryoSaveGameInfoWidget : public MOBase::ISaveGameInfoWidget +{ + Q_OBJECT + +public: + GamebryoSaveGameInfoWidget(GamebryoSaveGameInfo const* info, QWidget* parent); + ~GamebryoSaveGameInfoWidget(); + + virtual void setSave(MOBase::ISaveGame const&) override; + +private: + Ui::GamebryoSaveGameInfoWidget* ui; + GamebryoSaveGameInfo const* m_Info; +}; + +#endif // GAMEBRYOSAVEGAMEINFOWIDGET_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.ui b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.ui new file mode 100644 index 0000000..ea45d10 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.ui @@ -0,0 +1,209 @@ + + + GamebryoSaveGameInfoWidget + + + + 0 + 0 + 400 + 300 + + + + + 0 + 0 + + + + + + + + + + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + + true + + + + Save # + + + + + + + + true + + + + Character + + + + + + + + true + + + + Level + + + + + + + + true + + + + Location + + + + + + + + true + + + + Date + + + + + + + + 75 + true + + + + + + + + + + + + 75 + true + + + + + + + + + + + + 75 + true + + + + + + + + + + + + 75 + true + + + + + + + + + + + + 75 + true + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + false + + + + + + Qt::AlignCenter + + + + + + + + diff --git a/libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.cpp b/libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.cpp new file mode 100644 index 0000000..e3f709a --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.cpp @@ -0,0 +1,46 @@ +#include "gamebryoscriptextender.h" + +#include "gamegamebryo.h" + +#include "utility.h" + +#include +#include + +GamebryoScriptExtender::GamebryoScriptExtender(const GameGamebryo* game) : m_Game(game) +{} + +GamebryoScriptExtender::~GamebryoScriptExtender() {} + +QString GamebryoScriptExtender::loaderName() const +{ + return BinaryName(); +} + +QString GamebryoScriptExtender::loaderPath() const +{ + return m_Game->gameDirectory().absoluteFilePath(loaderName()); +} + +QString GamebryoScriptExtender::savegameExtension() const +{ + return m_Game->savegameSEExtension(); +} + +bool GamebryoScriptExtender::isInstalled() const +{ + // A note: It is possibly also OK if xxse_steam_loader.dll exists, but it's + // not clear why that would exist and the exe not if you'd installed it per + // instructions, and it'd mess up NCC installs a treat. + return m_Game->gameDirectory().exists(loaderName()); +} + +QString GamebryoScriptExtender::getExtenderVersion() const +{ + return MOBase::getFileVersion(m_Game->gameDirectory().absoluteFilePath(loaderName())); +} + +WORD GamebryoScriptExtender::getArch() const +{ + return m_Game->getArch(loaderName()); +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.h b/libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.h new file mode 100644 index 0000000..7ab4f1d --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.h @@ -0,0 +1,31 @@ +#ifndef GAMEBRYOSCRIPTEXTENDER_H +#define GAMEBRYOSCRIPTEXTENDER_H + +#include "scriptextender.h" + +class GameGamebryo; + +class GamebryoScriptExtender : public MOBase::ScriptExtender +{ +public: + GamebryoScriptExtender(GameGamebryo const* game); + + virtual ~GamebryoScriptExtender(); + + virtual QString loaderName() const override; + + virtual QString loaderPath() const override; + + virtual QString savegameExtension() const override; + + virtual bool isInstalled() const override; + + virtual QString getExtenderVersion() const override; + + virtual WORD getArch() const override; + +protected: + GameGamebryo const* const m_Game; +}; + +#endif // GAMEBRYOSCRIPTEXTENDER_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.cpp b/libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.cpp new file mode 100644 index 0000000..e5c7608 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.cpp @@ -0,0 +1,51 @@ +#include "gamebryounmanagedmods.h" +#include "gamegamebryo.h" +#include + +GamebryoUnmangedMods::GamebryoUnmangedMods(const GameGamebryo* game) : m_Game(game) {} + +GamebryoUnmangedMods::~GamebryoUnmangedMods() {} + +QStringList GamebryoUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList dlcPlugins = m_Game->DLCPlugins(); + QStringList mainPlugins = m_Game->primaryPlugins(); + + QDir dataDir(m_Game->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!mainPlugins.contains(fileName, Qt::CaseInsensitive) && + (!onlyOfficial || dlcPlugins.contains(fileName, Qt::CaseInsensitive))) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + + return result; +} + +QString GamebryoUnmangedMods::displayName(const QString& modName) const +{ + return modName; +} + +QFileInfo GamebryoUnmangedMods::referenceFile(const QString& modName) const +{ + QFileInfoList files = + m_Game->dataDirectory().entryInfoList(QStringList() << modName + ".es*"); + if (files.size() > 0) { + return files.at(0); + } else { + return QFileInfo(); + } +} + +QStringList GamebryoUnmangedMods::secondaryFiles(const QString& modName) const +{ + QStringList archives; + QDir dataDir = m_Game->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.bsa"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.h b/libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.h new file mode 100644 index 0000000..38e2ba5 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.h @@ -0,0 +1,26 @@ +#ifndef GAMEBRYOUNMANAGEDMODS_H +#define GAMEBRYOUNMANAGEDMODS_H + +#include + +class GameGamebryo; + +class GamebryoUnmangedMods : public MOBase::UnmanagedMods +{ +public: + GamebryoUnmangedMods(const GameGamebryo* game); + ~GamebryoUnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; + virtual QString displayName(const QString& modName) const override; + virtual QFileInfo referenceFile(const QString& modName) const override; + virtual QStringList secondaryFiles(const QString& modName) const override; + +protected: + const GameGamebryo* game() const { return m_Game; } + +private: + const GameGamebryo* m_Game; +}; + +#endif // GAMEBRYOUNMANAGEDMODS_H diff --git a/libs/game_gamebryo/src/gamebryo/gamegamebryo.cpp b/libs/game_gamebryo/src/gamebryo/gamegamebryo.cpp new file mode 100644 index 0000000..347c2d8 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamegamebryo.cpp @@ -0,0 +1,478 @@ +#include "gamegamebryo.h" + +#include "bsainvalidation.h" +#include "dataarchives.h" +#include "gamebryomoddatacontent.h" +#include "gamebryosavegame.h" +#include "gameplugins.h" +#include "iprofile.h" +#include "log.h" +#include "registry.h" +#include "savegameinfo.h" +#include "scopeguard.h" +#include "scriptextender.h" +#include "utility.h" +#include "vdf_parser.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +GameGamebryo::GameGamebryo() {} + +void GameGamebryo::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath(gameName()); +} + +bool GameGamebryo::init(MOBase::IOrganizer* moInfo) +{ + m_Organizer = moInfo; + m_Organizer->onAboutToRun([this](const auto& binary) { + return prepareIni(binary); + }); + return true; +} + +bool GameGamebryo::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +QIcon GameGamebryo::gameIcon() const +{ + return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(binaryName())); +} + +QDir GameGamebryo::gameDirectory() const +{ + return QDir(m_GamePath); +} + +QDir GameGamebryo::dataDirectory() const +{ + return gameDirectory().absoluteFilePath("data"); +} + +void GameGamebryo::setGamePath(const QString& path) +{ + m_GamePath = path; +} + +QDir GameGamebryo::documentsDirectory() const +{ + return m_MyGamesPath; +} + +QDir GameGamebryo::savesDirectory() const +{ + return QDir(myGamesPath() + "/Saves"); +} + +std::vector> +GameGamebryo::listSaves(QDir folder) const +{ + QStringList filters; + filters << QString("*.") + savegameExtension(); + + std::vector> saves; + for (auto info : folder.entryInfoList(filters, QDir::Files)) { + try { + saves.push_back(makeSaveGame(info.filePath())); + } catch (std::exception& e) { + MOBase::log::error("{}", e.what()); + continue; + } + } + + return saves; +} + +void GameGamebryo::setGameVariant(const QString& variant) +{ + m_GameVariant = variant; +} + +QString GameGamebryo::binaryName() const +{ + return gameShortName() + ".exe"; +} + +MOBase::IPluginGame::LoadOrderMechanism GameGamebryo::loadOrderMechanism() const +{ + return LoadOrderMechanism::FileTime; +} + +MOBase::IPluginGame::SortMechanism GameGamebryo::sortMechanism() const +{ + return SortMechanism::LOOT; +} + +bool GameGamebryo::looksValid(QDir const& path) const +{ + // Check for .exe for now. + return path.exists(binaryName()); +} + +QString GameGamebryo::gameVersion() const +{ + // We try the file version, but if it looks invalid (starts with the fallback + // version), we look the product version instead. If the product version is + // not empty, we use it. + QString binaryAbsPath = gameDirectory().absoluteFilePath(binaryName()); + QString version = MOBase::getFileVersion(binaryAbsPath); + if (version.startsWith(FALLBACK_GAME_VERSION)) { + QString pversion = MOBase::getProductVersion(binaryAbsPath); + if (!pversion.isEmpty()) { + version = pversion; + } + } + return version; +} + +QString GameGamebryo::getLauncherName() const +{ + return gameShortName() + "Launcher.exe"; +} + +WORD GameGamebryo::getArch(QString const& program) const +{ + WORD arch = 0; + // This *really* needs to be factored out + std::wstring app_name = + L"\\\\?\\" + + QDir::toNativeSeparators(this->gameDirectory().absoluteFilePath(program)) + .toStdWString(); + + WIN32_FIND_DATAW FindFileData; + HANDLE hFind = ::FindFirstFileW(app_name.c_str(), &FindFileData); + + // exit if the binary was not found + if (hFind == INVALID_HANDLE_VALUE) + return arch; + + HANDLE hFile = INVALID_HANDLE_VALUE; + HANDLE hMapping = INVALID_HANDLE_VALUE; + LPVOID addrHeader = nullptr; + PIMAGE_NT_HEADERS peHdr = nullptr; + + hFile = CreateFileW(app_name.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + if (hFile == INVALID_HANDLE_VALUE) + goto cleanup; + + hMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, + program.toStdWString().c_str()); + if (hMapping == INVALID_HANDLE_VALUE) + goto cleanup; + + addrHeader = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); + if (addrHeader == NULL) + goto cleanup; // couldn't memory map the file + + peHdr = ImageNtHeader(addrHeader); + if (peHdr == NULL) + goto cleanup; // couldn't read the header + + arch = peHdr->FileHeader.Machine; + +cleanup: // release all of our handles + FindClose(hFind); + if (hFile != INVALID_HANDLE_VALUE) + CloseHandle(hFile); + if (hMapping != INVALID_HANDLE_VALUE) + CloseHandle(hMapping); + return arch; +} + +QFileInfo GameGamebryo::findInGameFolder(const QString& relativePath) const +{ + return QFileInfo(m_GamePath + "/" + relativePath); +} + +QString GameGamebryo::identifyGamePath() const +{ + QString path = "Software\\Bethesda Softworks\\" + gameShortName(); + return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +} + +bool GameGamebryo::prepareIni(const QString& exec) +{ + MOBase::IProfile* profile = m_Organizer->profile(); + + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : documentsDirectory().absolutePath(); + + if (!iniFiles().isEmpty()) { + + QString profileIni = basePath + "/" + iniFiles()[0]; + + WCHAR setting[512]; + if (!GetPrivateProfileStringW(L"Launcher", L"bEnableFileSelection", L"0", setting, + 512, profileIni.toStdWString().c_str()) || + wcstol(setting, nullptr, 10) != 1) { + MOBase::WriteRegistryValue(L"Launcher", L"bEnableFileSelection", L"1", + profileIni.toStdWString().c_str()); + } + } + + return true; +} + +QString GameGamebryo::selectedVariant() const +{ + return m_GameVariant; +} + +QString GameGamebryo::myGamesPath() const +{ + return m_MyGamesPath; +} + +/*static*/ QString GameGamebryo::getLootPath() +{ + return findInRegistry(HKEY_LOCAL_MACHINE, L"Software\\LOOT", L"Installed Path") + + "/Loot.exe"; +} + +QString GameGamebryo::localAppFolder() +{ + QString result = getKnownFolderPath(FOLDERID_LocalAppData, false); + if (result.isEmpty()) { + // fallback: try the registry + result = getSpecialPath("Local AppData"); + } + return result; +} + +void GameGamebryo::copyToProfile(QString const& sourcePath, + QDir const& destinationDirectory, + QString const& sourceFileName) +{ + copyToProfile(sourcePath, destinationDirectory, sourceFileName, sourceFileName); +} + +void GameGamebryo::copyToProfile(QString const& sourcePath, + QDir const& destinationDirectory, + QString const& sourceFileName, + QString const& destinationFileName) +{ + QString filePath = destinationDirectory.absoluteFilePath(destinationFileName); + if (!QFileInfo(filePath).exists()) { + if (!MOBase::shellCopy(sourcePath + "/" + sourceFileName, filePath)) { + // if copy file fails, create the file empty + QFile(filePath).open(QIODevice::WriteOnly); + } + } +} + +MappingType GameGamebryo::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameShortName() + "/" + profileFile, + false}); + } + + return result; +} + +std::unique_ptr GameGamebryo::getRegValue(HKEY key, LPCWSTR path, LPCWSTR value, + DWORD flags, LPDWORD type = nullptr) +{ + DWORD size = 0; + HKEY subKey; + LONG res = ::RegOpenKeyExW(key, path, 0, KEY_QUERY_VALUE | KEY_WOW64_32KEY, &subKey); + if (res != ERROR_SUCCESS) { + res = ::RegOpenKeyExW(key, path, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &subKey); + if (res != ERROR_SUCCESS) + return std::unique_ptr(); + } + res = ::RegGetValueW(subKey, L"", value, flags, type, nullptr, &size); + if (res == ERROR_FILE_NOT_FOUND || res == ERROR_UNSUPPORTED_TYPE) { + return std::unique_ptr(); + } + if (res != ERROR_SUCCESS && res != ERROR_MORE_DATA) { + throw MOBase::MyException( + QObject::tr("failed to query registry path (preflight): %1").arg(res, 0, 16)); + } + + std::unique_ptr result(new BYTE[size]); + res = ::RegGetValueW(subKey, L"", value, flags, type, result.get(), &size); + + if (res != ERROR_SUCCESS) { + throw MOBase::MyException( + QObject::tr("failed to query registry path (read): %1").arg(res, 0, 16)); + } + + return result; +} + +QString GameGamebryo::findInRegistry(HKEY baseKey, LPCWSTR path, LPCWSTR value) +{ + std::unique_ptr buffer = + getRegValue(baseKey, path, value, RRF_RT_REG_SZ | RRF_NOEXPAND); + + return QString::fromUtf16(reinterpret_cast(buffer.get())); +} + +QString GameGamebryo::getKnownFolderPath(REFKNOWNFOLDERID folderId, bool useDefault) +{ + PWSTR path = nullptr; + ON_BLOCK_EXIT([&]() { + if (path != nullptr) + ::CoTaskMemFree(path); + }); + + if (::SHGetKnownFolderPath(folderId, useDefault ? KF_FLAG_DEFAULT_PATH : 0, NULL, + &path) == S_OK) { + return QDir::fromNativeSeparators(QString::fromWCharArray(path)); + } else { + return QString(); + } +} + +QString GameGamebryo::getSpecialPath(const QString& name) +{ + QString base = findInRegistry( + HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders", + name.toStdWString().c_str()); + + WCHAR temp[MAX_PATH]; + if (::ExpandEnvironmentStringsW(base.toStdWString().c_str(), temp, MAX_PATH) != 0) { + return QString::fromWCharArray(temp); + } else { + return base; + } +} + +QString GameGamebryo::determineMyGamesPath(const QString& gameName) +{ + const QString pattern = "%1/My Games/" + gameName; + + auto tryDir = [&](const QString& dir) -> std::optional { + if (dir.isEmpty()) { + return {}; + } + + const auto path = pattern.arg(dir); + if (!QFileInfo(path).exists()) { + return {}; + } + + return path; + }; + + // a) this is the way it should work. get the configured My Documents directory + if (auto d = tryDir(getKnownFolderPath(FOLDERID_Documents, false))) { + return *d; + } + + // b) if there is no directory there, look in the default directory + if (auto d = tryDir(getKnownFolderPath(FOLDERID_Documents, true))) { + return *d; + } + + // c) finally, look in the registry. This is discouraged + if (auto d = tryDir(getSpecialPath("Personal"))) { + return *d; + } + + return {}; +} + +QString GameGamebryo::parseEpicGamesLocation(const QStringList& manifests) +{ + // Use the registry entry to find the EGL Data dir first, just in case something + // changes + QString manifestDir = findInRegistry( + HKEY_LOCAL_MACHINE, L"Software\\Epic Games\\EpicGamesLauncher", L"AppDataPath"); + if (manifestDir.isEmpty()) + manifestDir = getKnownFolderPath(FOLDERID_ProgramData, false) + + "\\Epic\\EpicGamesLauncher\\Data\\"; + manifestDir += "Manifests"; + QDir epicManifests(manifestDir, "*.item", + QDir::SortFlags(QDir::Name | QDir::IgnoreCase), QDir::Files); + if (epicManifests.exists()) { + QDirIterator it(epicManifests); + while (it.hasNext()) { + QString manifestFile = it.next(); + QFile manifest(manifestFile); + + if (!manifest.open(QIODevice::ReadOnly)) { + qWarning("Couldn't open Epic Games manifest file."); + continue; + } + + QByteArray manifestData = manifest.readAll(); + + QJsonDocument manifestJson(QJsonDocument::fromJson(manifestData)); + + if (manifests.contains(manifestJson["AppName"].toString())) { + return manifestJson["InstallLocation"].toString(); + } + } + } + return ""; +} + +QString GameGamebryo::parseSteamLocation(const QString& appid, + const QString& directoryName) +{ + QString path = "Software\\Valve\\Steam"; + QString steamLocation = + findInRegistry(HKEY_CURRENT_USER, path.toStdWString().c_str(), L"SteamPath"); + if (!steamLocation.isEmpty()) { + QString steamLibraryLocation; + QString steamLibraries(steamLocation + "\\" + "config" + "\\" + + "libraryfolders.vdf"); + if (QFile(steamLibraries).exists()) { + std::ifstream file(steamLibraries.toStdString()); + auto root = tyti::vdf::read(file); + for (auto child : root.childs) { + tyti::vdf::object* library = child.second.get(); + auto apps = library->childs["apps"]; + if (apps->attribs.contains(appid.toStdString())) { + steamLibraryLocation = QString::fromStdString(library->attribs["path"]); + break; + } + } + } + if (!steamLibraryLocation.isEmpty()) { + QString gameLocation = steamLibraryLocation + "\\" + "steamapps" + "\\" + + "common" + "\\" + directoryName; + if (QDir(gameLocation).exists()) + return gameLocation; + } + } + return ""; +} + +void GameGamebryo::registerFeature(std::shared_ptr feature) +{ + // priority does not matter, this is a game plugin so will get lowest priority in MO2 + m_Organizer->gameFeatures()->registerFeature(this, feature, 0, true); +} diff --git a/libs/game_gamebryo/src/gamebryo/gamegamebryo.h b/libs/game_gamebryo/src/gamebryo/gamegamebryo.h new file mode 100644 index 0000000..2ff07db --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamegamebryo.h @@ -0,0 +1,151 @@ +#ifndef GAMEGAMEBRYO_H +#define GAMEGAMEBRYO_H + +#include "iplugingame.h" + +class BSAInvalidation; +class DataArchives; +class LocalSavegames; +class SaveGameInfo; +class BSAInvalidation; +class LocalSavegames; +class ScriptExtender; +class GamePlugins; +class UnmanagedMods; + +#include +#include +#include +#include +#include +#include +#include + +#include "gamebryosavegame.h" +#include "igamefeatures.h" + +class GameGamebryo : public MOBase::IPluginGame, public MOBase::IPluginFileMapper +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame MOBase::IPluginFileMapper) + + friend class GamebryoScriptExtender; + friend class GamebryoSaveGameInfo; + friend class GamebryoSaveGameInfoWidget; + friend class GamebryoSaveGame; + + /** + * Some Bethesda games do not have a valid file version but a valid product + * version. If the file version starts with FALLBACK_GAME_VERSION, the product + * version will be tried. + */ + static constexpr const char* FALLBACK_GAME_VERSION = "1.0.0"; + +public: + GameGamebryo(); + + void detectGame() override; + bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + // getName + // initializeProfile + virtual std::vector> + listSaves(QDir folder) const override; + + virtual bool isInstalled() const override; + virtual QIcon gameIcon() const override; + virtual QDir gameDirectory() const override; + virtual QDir dataDirectory() const override; + // secondaryDataDirectories + virtual void setGamePath(const QString& path) override; + virtual QDir documentsDirectory() const override; + virtual QDir savesDirectory() const override; + // executables + // steamAPPId + // primaryPlugins + // enabledPlugins + // gameVariants + virtual void setGameVariant(const QString& variant) override; + virtual QString binaryName() const override; + // gameShortName + // primarySources + // validShortNames + // iniFiles + // DLCPlugins + // CCPlugins + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual SortMechanism sortMechanism() const override; + // nexusModOrganizerID + // nexusGameID + virtual bool looksValid(QDir const&) const override; + virtual QString gameVersion() const override; + virtual QString getLauncherName() const override; + +public: // IPluginFileMapper interface + virtual MappingType mappings() const; + +public: // Other (e.g. for game features) + QString myGamesPath() const; + +protected: + // Retrieve the saves extension for the game. + virtual QString savegameExtension() const = 0; + virtual QString savegameSEExtension() const = 0; + + // Create a save game. + virtual std::shared_ptr + makeSaveGame(QString filepath) const = 0; + + QFileInfo findInGameFolder(const QString& relativePath) const; + QString selectedVariant() const; + WORD getArch(QString const& program) const; + + static QString localAppFolder(); + // Arguably this shouldn't really be here but every gamebryo program seems to + // use it + static QString getLootPath(); + + // This function is not terribly well named as it copies exactly where it's told + // to, irrespective of whether it's in the profile... + static void copyToProfile(const QString& sourcePath, const QDir& destinationDirectory, + const QString& sourceFileName); + + static void copyToProfile(const QString& sourcePath, const QDir& destinationDirectory, + const QString& sourceFileName, + const QString& destinationFileName); + + virtual QString identifyGamePath() const; + + virtual bool prepareIni(const QString& exec); + + static std::unique_ptr getRegValue(HKEY key, LPCWSTR path, LPCWSTR value, + DWORD flags, LPDWORD type); + + static QString findInRegistry(HKEY baseKey, LPCWSTR path, LPCWSTR value); + + static QString getKnownFolderPath(REFKNOWNFOLDERID folderId, bool useDefault); + + static QString getSpecialPath(const QString& name); + + static QString determineMyGamesPath(const QString& gameName); + + static QString parseEpicGamesLocation(const QStringList& manifests); + + static QString parseSteamLocation(const QString& appid, const QString& directoryName); + +protected: + void registerFeature(std::shared_ptr feature); + +protected: + // to access organizer for game features, avoid having to pass it to all saves since + // we already pass the game + friend class GamebryoSaveGame; + + QString m_GamePath; + QString m_MyGamesPath; + QString m_GameVariant; + MOBase::IOrganizer* m_Organizer; +}; + +#endif // GAMEGAMEBRYO_H diff --git a/libs/game_gamebryo/src/gamebryo/vdf_parser.h b/libs/game_gamebryo/src/gamebryo/vdf_parser.h new file mode 100644 index 0000000..32d4d27 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/vdf_parser.h @@ -0,0 +1,739 @@ +// MIT License +// +// Copyright(c) 2016 Matthias Moeller +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef __TYTI_STEAM_VDF_PARSER_H__ +#define __TYTI_STEAM_VDF_PARSER_H__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// for wstring support +#include +#include + +// internal +#include + +// VS < 2015 has only partial C++11 support +#if defined(_MSC_VER) && _MSC_VER < 1900 +#ifndef CONSTEXPR +#define CONSTEXPR +#endif + +#ifndef NOEXCEPT +#define NOEXCEPT +#endif +#else +#ifndef CONSTEXPR +#define CONSTEXPR constexpr +#define TYTI_UNDEF_CONSTEXPR +#endif + +#ifndef NOEXCEPT +#define NOEXCEPT noexcept +#define TYTI_UNDEF_NOEXCEPT +#endif + +#endif + +namespace tyti +{ +namespace vdf +{ + namespace detail + { + /////////////////////////////////////////////////////////////////////////// + // Helper functions selecting the right encoding (char/wchar_T) + /////////////////////////////////////////////////////////////////////////// + + template + struct literal_macro_help + { + static CONSTEXPR const char* result(const char* c, const wchar_t*) NOEXCEPT + { + return c; + } + static CONSTEXPR const char result(const char c, const wchar_t) NOEXCEPT + { + return c; + } + }; + + template <> + struct literal_macro_help + { + static CONSTEXPR const wchar_t* result(const char*, const wchar_t* wc) NOEXCEPT + { + return wc; + } + static CONSTEXPR const wchar_t result(const char, const wchar_t wc) NOEXCEPT + { + return wc; + } + }; +#define TYTI_L(type, text) vdf::detail::literal_macro_help::result(text, L##text) + + inline std::string string_converter(const std::string& w) NOEXCEPT + { + return w; + } + + // utility wrapper to adapt locale-bound facets for wstring/wbuffer convert + // from cppreference + template + struct deletable_facet : Facet + { + template + deletable_facet(Args&&... args) : Facet(std::forward(args)...) + {} + ~deletable_facet() {} + }; + + inline std::string string_converter(const std::wstring& w) // todo: use us-locale + { + std::wstring_convert>> + conv1; + return conv1.to_bytes(w); + } + + /////////////////////////////////////////////////////////////////////////// + // Writer helper functions + /////////////////////////////////////////////////////////////////////////// + + template + class tabs + { + const size_t t; + + public: + explicit CONSTEXPR tabs(size_t i) NOEXCEPT : t(i) {} + std::basic_string print() const + { + return std::basic_string(t, TYTI_L(charT, '\t')); + } + inline CONSTEXPR tabs operator+(size_t i) const NOEXCEPT { return tabs(t + i); } + }; + + template + oStreamT& operator<<(oStreamT& s, const tabs t) + { + s << t.print(); + return s; + } + } // end namespace detail + + /////////////////////////////////////////////////////////////////////////// + // Interface + /////////////////////////////////////////////////////////////////////////// + + /// custom objects and their corresponding write functions + + /// basic object node. Every object has a name and can contains attributes saved as + /// key_value pairs or childrens + template + struct basic_object + { + typedef CharT char_type; + std::basic_string name; + std::unordered_map, std::basic_string> + attribs; + std::unordered_map, + std::shared_ptr>> + childs; + + void add_attribute(std::basic_string key, + std::basic_string value) + { + attribs.emplace(std::move(key), std::move(value)); + } + void add_child(std::unique_ptr> child) + { + std::shared_ptr> obj{child.release()}; + childs.emplace(obj->name, obj); + } + void set_name(std::basic_string n) { name = std::move(n); } + }; + + template + struct basic_multikey_object + { + typedef CharT char_type; + std::basic_string name; + std::unordered_multimap, std::basic_string> + attribs; + std::unordered_multimap, + std::shared_ptr>> + childs; + + void add_attribute(std::basic_string key, + std::basic_string value) + { + attribs.emplace(std::move(key), std::move(value)); + } + void add_child(std::unique_ptr> child) + { + std::shared_ptr> obj{child.release()}; + childs.emplace(obj->name, obj); + } + void set_name(std::basic_string n) { name = std::move(n); } + }; + + typedef basic_object object; + typedef basic_object wobject; + typedef basic_multikey_object multikey_object; + typedef basic_multikey_object wmultikey_object; + + struct Options + { + bool strip_escape_symbols; + bool ignore_all_platform_conditionals; + bool ignore_includes; + + Options() + : strip_escape_symbols(true), ignore_all_platform_conditionals(false), + ignore_includes(false) + {} + }; + + // forward decls + // forward decl + template + OutputT read(iStreamT& inStream, const Options& opt = Options{}); + + /** \brief writes given object tree in vdf format to given stream. + Output is prettyfied, using tabs + */ + template + void write(oStreamT& s, const T& r, + const detail::tabs tab = + detail::tabs(0)) + { + typedef typename oStreamT::char_type charT; + using namespace detail; + s << tab << TYTI_L(charT, '"') << r.name << TYTI_L(charT, "\"\n") << tab + << TYTI_L(charT, "{\n"); + for (const auto& i : r.attribs) + s << tab + 1 << TYTI_L(charT, '"') << i.first << TYTI_L(charT, "\"\t\t\"") + << i.second << TYTI_L(charT, "\"\n"); + for (const auto& i : r.childs) + if (i.second) + write(s, *i.second, tab + 1); + s << tab << TYTI_L(charT, "}\n"); + } + + namespace detail + { + template + std::basic_string read_file(iStreamT& inStream) + { + // cache the file + typedef typename iStreamT::char_type charT; + std::basic_string str; + inStream.seekg(0, std::ios::end); + str.resize(static_cast(inStream.tellg())); + if (str.empty()) + return str; + + inStream.seekg(0, std::ios::beg); + inStream.read(&str[0], str.size()); + return str; + } + + /** \brief Read VDF formatted sequences defined by the range [first, last). + If the file is mailformatted, parser will try to read it until it can. + @param first begin iterator + @param end end iterator + @param exclude_files list of files which cant be included anymore. + prevents circular includes + + can thow: + - "std::runtime_error" if a parsing error occured + - "std::bad_alloc" if not enough memory coup be allocated + */ + template + std::vector> + read_internal(IterT first, const IterT last, + std::unordered_set::value_type>>& exclude_files, + const Options& opt) + { + static_assert(std::is_default_constructible::value, + "Output Type must be default constructible (provide constructor " + "without arguments)"); + static_assert(std::is_move_constructible::value, + "Output Type must be move constructible"); + + typedef typename std::iterator_traits::value_type charT; + + const std::basic_string comment_end_str = TYTI_L(charT, "*/"); + const std::basic_string whitespaces = TYTI_L(charT, " \n\v\f\r\t"); + +#ifdef WIN32 + std::function&)> is_platform_str = + [](const std::basic_string& in) { + return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$WINDOWS"); + }; +#elif __APPLE__ + // WIN32 stands for pc in general + std::function&)> is_platform_str = + [](const std::basic_string& in) { + return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$POSIX") || + in == TYTI_L(charT, "$OSX"); + }; + +#elif __linux__ + // WIN32 stands for pc in general + std::function&)> is_platform_str = + [](const std::basic_string& in) { + return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$POSIX") || + in == TYTI_L(charT, "$LINUX"); + }; +#else + std::function&)> is_platform_str = + [](const std::basic_string& in) { + return false; + }; +#endif + + if (opt.ignore_all_platform_conditionals) + is_platform_str = [](const std::basic_string&) { + return false; + }; + + // function for skipping a comment block + // iter: iterator poition to the position after a '/' + auto skip_comments = [&comment_end_str](IterT iter, const IterT& last) -> IterT { + ++iter; + if (iter != last) { + if (*iter == TYTI_L(charT, '/')) { + // line comment, skip whole line + iter = std::find(iter + 1, last, TYTI_L(charT, '\n')); + } + + if (*iter == '*') { + // block comment, skip until next occurance of "*\" + iter = std::search(iter + 1, last, std::begin(comment_end_str), + std::end(comment_end_str)); + iter += 2; + } + } + return iter; + }; + + auto end_quote = [](IterT iter, const IterT& last) -> IterT { + const auto begin = iter; + auto last_esc = iter; + do { + ++iter; + iter = std::find(iter, last, TYTI_L(charT, '\"')); + if (iter == last) + break; + + last_esc = std::prev(iter); + while (last_esc != begin && *last_esc == '\\') + --last_esc; + } while (!(std::distance(last_esc, iter) % 2)); + if (iter == last) + throw std::runtime_error{"quote was opened but not closed."}; + return iter; + }; + + auto end_word = [&whitespaces](IterT iter, const IterT& last) -> IterT { + const auto begin = iter; + auto last_esc = iter; + do { + ++iter; + iter = std::find_first_of(iter, last, std::begin(whitespaces), + std::end(whitespaces)); + if (iter == last) + break; + + last_esc = std::prev(iter); + while (last_esc != begin && *last_esc == '\\') + --last_esc; + } while (!(std::distance(last_esc, iter) % 2)); + // if (iter == last) + // throw std::runtime_error{ "word wasnt properly ended" }; + return iter; + }; + + auto skip_whitespaces = [&whitespaces](IterT iter, const IterT& last) -> IterT { + iter = std::find_if_not(iter, last, [&whitespaces](charT c) { + // return true if whitespace + return std::any_of(std::begin(whitespaces), std::end(whitespaces), + [c](charT pc) { + return pc == c; + }); + }); + return iter; + }; + + std::function&)> strip_escape_symbols = + [](std::basic_string& s) { + auto quote_searcher = [&s](size_t pos) { + return s.find(TYTI_L(charT, "\\\""), pos); + }; + auto p = quote_searcher(0); + while (p != s.npos) { + s.replace(p, 2, TYTI_L(charT, "\"")); + p = quote_searcher(p); + } + auto searcher = [&s](size_t pos) { + return s.find(TYTI_L(charT, "\\\\"), pos); + }; + p = searcher(0); + while (p != s.npos) { + s.replace(p, 2, TYTI_L(charT, "\\")); + p = searcher(p); + } + }; + + if (!opt.strip_escape_symbols) + strip_escape_symbols = [](std::basic_string&) {}; + + auto conditional_fullfilled = [&skip_whitespaces, + &is_platform_str](IterT& iter, const IterT& last) { + iter = skip_whitespaces(iter, last); + if (*iter == '[') { + ++iter; + const auto end = std::find(iter, last, ']'); + const bool negate = *iter == '!'; + if (negate) + ++iter; + auto conditional = std::basic_string(iter, end); + + const bool is_platform = is_platform_str(conditional); + iter = end + 1; + + return static_cast(is_platform ^ negate); + } + return true; + }; + + // read header + // first, quoted name + std::unique_ptr curObj = nullptr; + std::vector> roots; + std::stack> lvls; + auto curIter = first; + + while (curIter != last && *curIter != '\0') { + // find first starting attrib/child, or ending + curIter = skip_whitespaces(curIter, last); + if (curIter == last || *curIter == '\0') + break; + if (*curIter == TYTI_L(charT, '/')) { + curIter = skip_comments(curIter, last); + } else if (*curIter != TYTI_L(charT, '}')) { + + // get key + const auto keyEnd = (*curIter == TYTI_L(charT, '\"')) + ? end_quote(curIter, last) + : end_word(curIter, last); + if (*curIter == TYTI_L(charT, '\"')) + ++curIter; + std::basic_string key(curIter, keyEnd); + strip_escape_symbols(key); + curIter = keyEnd + ((*keyEnd == TYTI_L(charT, '\"')) ? 1 : 0); + + curIter = skip_whitespaces(curIter, last); + + auto conditional = conditional_fullfilled(curIter, last); + if (!conditional) + continue; + + while (*curIter == TYTI_L(charT, '/')) { + + curIter = skip_comments(curIter, last); + if (curIter == last || *curIter == '}') + throw std::runtime_error{"key declared, but no value"}; + curIter = skip_whitespaces(curIter, last); + if (curIter == last || *curIter == '}') + throw std::runtime_error{"key declared, but no value"}; + } + // get value + if (*curIter != '{') { + const auto valueEnd = (*curIter == TYTI_L(charT, '\"')) + ? end_quote(curIter, last) + : end_word(curIter, last); + if (*curIter == TYTI_L(charT, '\"')) + ++curIter; + + auto value = std::basic_string(curIter, valueEnd); + strip_escape_symbols(value); + curIter = valueEnd + ((*valueEnd == TYTI_L(charT, '\"')) ? 1 : 0); + + auto conditional = conditional_fullfilled(curIter, last); + if (!conditional) + continue; + + // process value + if (key != TYTI_L(charT, "#include") && key != TYTI_L(charT, "#base")) { + if (curObj) { + curObj->add_attribute(std::move(key), std::move(value)); + } else { + throw std::runtime_error{"unexpected key without object"}; + } + } else { + if (!opt.ignore_includes && + exclude_files.find(value) == exclude_files.end()) { + exclude_files.insert(value); + std::basic_ifstream i(detail::string_converter(value)); + auto str = read_file(i); + auto file_objs = + read_internal(str.begin(), str.end(), exclude_files, opt); + for (auto& n : file_objs) { + if (curObj) + curObj->add_child(std::move(n)); + else + roots.push_back(std::move(n)); + } + exclude_files.erase(value); + } + } + } else if (*curIter == '{') { + if (curObj) + lvls.push(std::move(curObj)); + curObj = std::make_unique(); + curObj->set_name(std::move(key)); + ++curIter; + } + } + // end of new object + else if (curObj && *curIter == TYTI_L(charT, '}')) { + if (!lvls.empty()) { + // get object before + std::unique_ptr prev{std::move(lvls.top())}; + lvls.pop(); + + // add finished obj to obj before and release it from processing + prev->add_child(std::move(curObj)); + curObj = std::move(prev); + } else { + roots.push_back(std::move(curObj)); + curObj.reset(); + } + ++curIter; + } else { + throw std::runtime_error{"unexpected '}'"}; + } + } + if (curObj != nullptr || !lvls.empty()) { + throw std::runtime_error{"object is not closed with '}'"}; + } + + return roots; + } + + } // namespace detail + + /** \brief Read VDF formatted sequences defined by the range [first, last). + If the file is mailformatted, parser will try to read it until it can. + @param first begin iterator + @param end end iterator + + can thow: + - "std::runtime_error" if a parsing error occured + - "std::bad_alloc" if not enough memory coup be allocated + */ + template + OutputT read(IterT first, const IterT last, const Options& opt = Options{}) + { + auto exclude_files = std::unordered_set< + std::basic_string::value_type>>{}; + auto roots = detail::read_internal(first, last, exclude_files, opt); + + OutputT result; + if (roots.size() > 1) { + for (auto& i : roots) + result.add_child(std::move(i)); + } else if (roots.size() == 1) + result = std::move(*roots[0]); + + return result; + } + + /** \brief Read VDF formatted sequences defined by the range [first, last). + If the file is mailformatted, parser will try to read it until it can. + @param first begin iterator + @param end end iterator + @param ec output bool. 0 if ok, otherwise, holds an system error code + + Possible error codes: + std::errc::protocol_error: file is mailformatted + std::errc::not_enough_memory: not enough space + std::errc::invalid_argument: iterators throws e.g. out of range + */ + template + OutputT read(IterT first, IterT last, std::error_code& ec, + const Options& opt = Options{}) NOEXCEPT + + { + ec.clear(); + OutputT r{}; + try { + r = read(first, last, opt); + } catch (std::runtime_error&) { + ec = std::make_error_code(std::errc::protocol_error); + } catch (std::bad_alloc&) { + ec = std::make_error_code(std::errc::not_enough_memory); + } catch (...) { + ec = std::make_error_code(std::errc::invalid_argument); + } + return r; + } + + /** \brief Read VDF formatted sequences defined by the range [first, last). + If the file is mailformatted, parser will try to read it until it can. + @param first begin iterator + @param end end iterator + @param ok output bool. true, if parser successed, false, if parser failed + */ + template + OutputT read(IterT first, const IterT last, bool* ok, + const Options& opt = Options{}) NOEXCEPT + { + std::error_code ec; + auto r = read(first, last, ec, opt); + if (ok) + *ok = !ec; + return r; + } + + template + inline auto read(IterT first, const IterT last, bool* ok, + const Options& opt = Options{}) NOEXCEPT + ->basic_object::value_type> + { + return read::value_type>>( + first, last, ok, opt); + } + + template + inline auto read(IterT first, IterT last, std::error_code& ec, + const Options& opt = Options{}) NOEXCEPT + ->basic_object::value_type> + { + return read::value_type>>( + first, last, ec, opt); + } + + template + inline auto read(IterT first, const IterT last, const Options& opt = Options{}) + -> basic_object::value_type> + { + return read::value_type>>( + first, last, opt); + } + + /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf + formatted data. throws "std::bad_alloc" if file buffer could not be allocated + */ + template + OutputT read(iStreamT& inStream, std::error_code& ec, const Options& opt = Options{}) + { + // cache the file + typedef typename iStreamT::char_type charT; + std::basic_string str = detail::read_file(inStream); + + // parse it + return read(str.begin(), str.end(), ec, opt); + } + + template + inline basic_object + read(iStreamT& inStream, std::error_code& ec, const Options& opt = Options{}) + { + return read>(inStream, ec, opt); + } + + /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf + formatted data. throws "std::bad_alloc" if file buffer could not be allocated ok == + false, if a parsing error occured + */ + template + OutputT read(iStreamT& inStream, bool* ok, const Options& opt = Options{}) + { + std::error_code ec; + const auto r = read(inStream, ec, opt); + if (ok) + *ok = !ec; + return r; + } + + template + inline basic_object read(iStreamT& inStream, bool* ok, + const Options& opt = Options{}) + { + return read>(inStream, ok, opt); + } + + /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf + formatted data. throws "std::bad_alloc" if file buffer could not be allocated + throws "std::runtime_error" if a parsing error occured + */ + template + OutputT read(iStreamT& inStream, const Options& opt) + { + + // cache the file + typedef typename iStreamT::char_type charT; + std::basic_string str = detail::read_file(inStream); + // parse it + return read(str.begin(), str.end(), opt); + } + + template + inline basic_object read(iStreamT& inStream, + const Options& opt = Options{}) + { + return read>(inStream, opt); + } + +} // namespace vdf +} // namespace tyti +#ifndef TYTI_NO_L_UNDEF +#undef TYTI_L +#endif + +#ifdef TYTI_UNDEF_CONSTEXPR +#undef CONSTEXPR +#undef TYTI_NO_L_UNDEF +#endif + +#ifdef TYTI_UNDEF_NOTHROW +#undef NOTHROW +#undef TYTI_UNDEF_NOTHROW +#endif + +#endif //__TYTI_STEAM_VDF_PARSER_H__ diff --git a/libs/installer_bain/.clang-format b/libs/installer_bain/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/installer_bain/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/installer_bain/.git-blame-ignore-revs b/libs/installer_bain/.git-blame-ignore-revs new file mode 100644 index 0000000..d0c3535 --- /dev/null +++ b/libs/installer_bain/.git-blame-ignore-revs @@ -0,0 +1 @@ +60e7642364b534d9cb405acfa750b7bbe73869a9 diff --git a/libs/installer_bain/.gitattributes b/libs/installer_bain/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/installer_bain/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/installer_bain/.github/workflows/build.yml b/libs/installer_bain/.github/workflows/build.yml new file mode 100644 index 0000000..e7f10a1 --- /dev/null +++ b/libs/installer_bain/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build Installer BAIN + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Installer BAIN + id: build-installer-bain + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase diff --git a/libs/installer_bain/.github/workflows/linting.yml b/libs/installer_bain/.github/workflows/linting.yml new file mode 100644 index 0000000..75c294e --- /dev/null +++ b/libs/installer_bain/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Installer BAIN Plugin + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/installer_bain/.gitignore b/libs/installer_bain/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/installer_bain/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/installer_bain/.pre-commit-config.yaml b/libs/installer_bain/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/installer_bain/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/installer_bain/CMakeLists.txt b/libs/installer_bain/CMakeLists.txt new file mode 100644 index 0000000..4834057 --- /dev/null +++ b/libs/installer_bain/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(installer_bain) + +add_subdirectory(src) diff --git a/libs/installer_bain/CMakePresets.json b/libs/installer_bain/CMakePresets.json new file mode 100644 index 0000000..5a63c77 --- /dev/null +++ b/libs/installer_bain/CMakePresets.json @@ -0,0 +1,67 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/installer_bain/src/CMakeLists.txt b/libs/installer_bain/src/CMakeLists.txt new file mode 100644 index 0000000..de57063 --- /dev/null +++ b/libs/installer_bain/src/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB installer_bain_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(installer_bain SHARED ${installer_bain_SOURCES}) +mo2_configure_plugin(installer_bain NO_SOURCES WARNINGS OFF) +target_link_libraries(installer_bain PRIVATE mo2::uibase) +mo2_install_plugin(installer_bain) diff --git a/libs/installer_bain/src/baincomplexinstallerdialog.cpp b/libs/installer_bain/src/baincomplexinstallerdialog.cpp new file mode 100644 index 0000000..2c25d76 --- /dev/null +++ b/libs/installer_bain/src/baincomplexinstallerdialog.cpp @@ -0,0 +1,121 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ +#include "ui_baincomplexinstallerdialog.h" + +#include + +#include + +#include "baincomplexinstallerdialog.h" + +using namespace MOBase; + +BainComplexInstallerDialog::BainComplexInstallerDialog( + const std::vector>& subpackages, + const GuessedValue& modName, const QStringList& defaultOptions, + const QString& packageTXT, QWidget* parent) + : TutorableDialog("BainInstaller", parent), ui(new Ui::BainComplexInstallerDialog), + m_Manual(false), m_PackageTXT(packageTXT) +{ + ui->setupUi(this); + + for (auto iter = modName.variants().begin(); iter != modName.variants().end(); + ++iter) { + ui->nameCombo->addItem(*iter); + } + + ui->nameCombo->setCurrentIndex(ui->nameCombo->findText(modName)); + + for (const auto& subpackage : subpackages) { + + auto name = subpackage->name(); + + QListWidgetItem* item = new QListWidgetItem(name, ui->optionsList); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + if (name.mid(0, 2) == "00" || defaultOptions.contains(name, Qt::CaseInsensitive)) { + item->setCheckState(Qt::Checked); + } else { + item->setCheckState(Qt::Unchecked); + } + + ui->optionsList->addItem(item); + } + + ui->packageBtn->setEnabled(!packageTXT.isEmpty()); + ui->nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); +} + +BainComplexInstallerDialog::~BainComplexInstallerDialog() +{ + delete ui; +} + +QString BainComplexInstallerDialog::getName() const +{ + return ui->nameCombo->currentText(); +} + +QStringList BainComplexInstallerDialog::updateTree(std::shared_ptr& tree) +{ + // Retrieve the list of selected names: + std::set selectedNames; + for (int i = 0; i < ui->optionsList->count(); ++i) { + QListWidgetItem* item = ui->optionsList->item(i); + if (item->checkState() == Qt::Checked) { + selectedNames.insert(item->text()); + } + } + + // Create a new empty tree and merge all the selected folder in it: + auto newTree = tree->createOrphanTree(); + for (auto& entry : *tree) { + if (entry->isDir() && selectedNames.count(entry->name()) > 0) { + newTree->merge(entry->astree()); + } + } + + tree = newTree; + + return QStringList(selectedNames.begin(), selectedNames.end()); +} + +void BainComplexInstallerDialog::on_okBtn_clicked() +{ + this->accept(); +} + +void BainComplexInstallerDialog::on_cancelBtn_clicked() +{ + this->reject(); +} + +void BainComplexInstallerDialog::on_manualBtn_clicked() +{ + m_Manual = true; + this->reject(); +} + +void BainComplexInstallerDialog::on_packageBtn_clicked() +{ + TextViewer viewer(m_PackageTXT, this); + viewer.setDescription(""); + viewer.addFile(m_PackageTXT, false); + viewer.exec(); +} diff --git a/libs/installer_bain/src/baincomplexinstallerdialog.h b/libs/installer_bain/src/baincomplexinstallerdialog.h new file mode 100644 index 0000000..f657627 --- /dev/null +++ b/libs/installer_bain/src/baincomplexinstallerdialog.h @@ -0,0 +1,94 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef BAINCOMPLEXINSTALLERDIALOG_H +#define BAINCOMPLEXINSTALLERDIALOG_H + +#include +#include +#include + +namespace Ui +{ +class BainComplexInstallerDialog; +} + +/** + * @brief Dialog to choose from options offered by a (complex) bain package + **/ +class BainComplexInstallerDialog : public MOBase::TutorableDialog +{ + Q_OBJECT + +public: + /** + * @brief Constructor + * + * @param tree the directory tree of the archive. The caller is resonsible to verify + *this actually qualifies as a bain installer + * @param modName proposed name for the mod. The dialog allows the user to change this + * @param defaultOptions The default options to select. Can contains options not + *actually present. + * @param packageTXT path to the extracted package.txt file or an empty string if + *there is none + * @param parent parent widget + **/ + explicit BainComplexInstallerDialog( + const std::vector>& subpackages, + const MOBase::GuessedValue& modName, const QStringList& defaultOptions, + const QString& packageTXT, QWidget* parent); + ~BainComplexInstallerDialog(); + + /** + * @return bool true if the user requested the manual dialog + **/ + bool manualRequested() const { return m_Manual; } + + /** + * @return QString the (user-modified) name to be used for the mod + **/ + QString getName() const; + + /** + * @brief Remove from the given tree the option not selected by the user. + * + * @param tree The input tree. + * + * @return the list of options selected by the user. + **/ + QStringList updateTree(std::shared_ptr& tree); + +private slots: + + void on_manualBtn_clicked(); + + void on_okBtn_clicked(); + + void on_cancelBtn_clicked(); + + void on_packageBtn_clicked(); + +private: + Ui::BainComplexInstallerDialog* ui; + + bool m_Manual; + QString m_PackageTXT; +}; + +#endif // BAINCOMPLEXINSTALLERDIALOG_H diff --git a/libs/installer_bain/src/baincomplexinstallerdialog.ui b/libs/installer_bain/src/baincomplexinstallerdialog.ui new file mode 100644 index 0000000..0ab5214 --- /dev/null +++ b/libs/installer_bain/src/baincomplexinstallerdialog.ui @@ -0,0 +1,126 @@ + + + BainComplexInstallerDialog + + + + 0 + 0 + 424 + 365 + + + + BAIN Package Installer + + + + + + + + Name + + + + + + + true + + + + + + + + + This looks like a Package prepared for Installation through BAIN. You can select options from the list below. If there is a package.txt file, it should contain detailed information about the options. + + + true + + + + + + + Components of this package. + + + Components of this package. +If there is a component called "00 Core" it is usually required. Options are ordered by priority as set up by the author. + + + + + + + + + Opens a Dialog that allows custom modifications. + + + Opens a Dialog that allows custom modifications. + + + Manual + + + false + + + + + + + The package.txt is often part of BAIN packages and contains details about the options available. + + + The package.txt is often part of BAIN packages and contains details about the options available. + + + Package.txt + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Ok + + + + + + + Cancel + + + false + + + + + + + + + + diff --git a/libs/installer_bain/src/installer_bain_en.ts b/libs/installer_bain/src/installer_bain_en.ts new file mode 100644 index 0000000..fe78e5d --- /dev/null +++ b/libs/installer_bain/src/installer_bain_en.ts @@ -0,0 +1,83 @@ + + + + + BainComplexInstallerDialog + + + BAIN Package Installer + + + + + Name + + + + + This looks like a Package prepared for Installation through BAIN. You can select options from the list below. If there is a package.txt file, it should contain detailed information about the options. + + + + + Components of this package. + + + + + Components of this package. +If there is a component called "00 Core" it is usually required. Options are ordered by priority as set up by the author. + + + + + + Opens a Dialog that allows custom modifications. + + + + + Manual + + + + + + The package.txt is often part of BAIN packages and contains details about the options available. + + + + + Ok + + + + + Cancel + + + + + InstallerBAIN + + + BAIN Installer + + + + + Installer for BAIN archives (originally targeting Wrye Bash) + + + + + May be BAIN installer + + + + + This installer looks like it may contain a BAIN installer but I'm not sure. Install as BAIN installer? + + + + diff --git a/libs/installer_bain/src/installerbain.cpp b/libs/installer_bain/src/installerbain.cpp new file mode 100644 index 0000000..d39590e --- /dev/null +++ b/libs/installer_bain/src/installerbain.cpp @@ -0,0 +1,222 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "installerbain.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "baincomplexinstallerdialog.h" + +using namespace MOBase; + +InstallerBAIN::InstallerBAIN() : m_MOInfo(nullptr) {} + +bool InstallerBAIN::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + return true; +} + +QString InstallerBAIN::name() const +{ + return "BAIN Installer"; +} + +QString InstallerBAIN::localizedName() const +{ + return tr("BAIN Installer"); +} + +QString InstallerBAIN::author() const +{ + return "Tannin"; +} + +QString InstallerBAIN::description() const +{ + return tr("Installer for BAIN archives (originally targeting Wrye Bash)"); +} + +VersionInfo InstallerBAIN::version() const +{ + return VersionInfo(1, 1, 0, VersionInfo::RELEASE_FINAL); +} + +QList InstallerBAIN::settings() const +{ + return QList(); +} + +unsigned int InstallerBAIN::priority() const +{ + return 40; +} + +bool InstallerBAIN::isManualInstaller() const +{ + return false; +} + +void InstallerBAIN::onInstallationStart(QString const&, bool, IModInterface* currentMod) +{ + // We reset some field and fetch the previously installed options: + m_InstallerUsed = false; + m_PreviousOptions.clear(); + m_SelectedOptions.clear(); + + if (currentMod) { + auto settings = currentMod->pluginSettings(name()); + for (auto& [name, value] : settings) { + if (name.startsWith("option")) { + m_PreviousOptions.append(value.toString()); + } + } + } +} + +void InstallerBAIN::onInstallationEnd(EInstallResult result, IModInterface* newMod) +{ + if (result == EInstallResult::RESULT_SUCCESS && m_InstallerUsed) { + newMod->clearPluginSettings(name()); + for (auto i = 0; i < m_SelectedOptions.size(); ++i) { + newMod->setPluginSetting(name(), QString("option%1").arg(i), + m_SelectedOptions[i]); + } + } +} + +std::vector> +InstallerBAIN::findSubpackages(std::shared_ptr tree, + std::size_t* invalidFolders) const +{ + // Folders that can be present but do not impact the installation: + static const std::set IGNORED_FOLDERS{ + "fomod", "omod conversion data", "images", "screenshots", "docs"}; + + auto checker = m_MOInfo->gameFeatures()->gameFeature(); + + if (!checker) { + return {}; + } + + // each directory would have to serve as a top-level directory + std::vector> subpackages; + std::size_t ninvalids = 0; + for (auto entry : *tree) { + + if (!entry->isDir()) { + continue; + } + + // ignore fomod in case of combined fomod/bain packages. + // dirs starting with -- are supposed to be ignored + if (IGNORED_FOLDERS.contains(entry->name()) || entry->name().startsWith("--")) { + continue; + } + + if (checker->dataLooksValid(entry->astree()) == + ModDataChecker::CheckReturn::VALID) { + subpackages.push_back(entry); + } else { + ninvalids++; + } + } + + if (invalidFolders) { + *invalidFolders = ninvalids; + } + + return subpackages; +} + +bool InstallerBAIN::isArchiveSupported(std::shared_ptr tree) const +{ + auto checker = m_MOInfo->gameFeatures()->gameFeature(); + + if (!checker) { + return false; + } + + std::size_t numInvalidDirs = 0; + auto subpackages = findSubpackages(tree, &numInvalidDirs); + + if (subpackages.size() < 2) { + // a complex bain package contains at least 2 directories to choose from + return false; + } else if (numInvalidDirs == 0) { + return true; + } else { + return QMessageBox::question(parentWidget(), tr("May be BAIN installer"), + tr("This installer looks like it may contain a BAIN " + "installer but I'm not sure. " + "Install as BAIN installer?"), + QMessageBox::Yes | QMessageBox::No) == + QMessageBox::Yes; + } + + return true; +} + +IPluginInstaller::EInstallResult +InstallerBAIN::install(GuessedValue& modName, std::shared_ptr& tree, + QString&, int&) +{ + auto entry = tree->find("package.txt", FileTreeEntry::FILE); + + QString packageTXT; + if (entry != nullptr) { + packageTXT = manager()->extractFile(entry); + } + + auto subpackages = findSubpackages(tree); + + BainComplexInstallerDialog dialog(subpackages, modName, m_PreviousOptions, packageTXT, + parentWidget()); + + int res = dialog.exec(); + + if (res == QDialog::Accepted) { + modName.update(dialog.getName(), GUESS_USER); + + // Update the tree: + m_SelectedOptions = dialog.updateTree(tree); + m_InstallerUsed = true; + + return IPluginInstaller::RESULT_SUCCESS; + } else { + if (dialog.manualRequested()) { + modName.update(dialog.getName(), GUESS_USER); + return IPluginInstaller::RESULT_MANUALREQUESTED; + } else { + return IPluginInstaller::RESULT_CANCELED; + } + } +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(installerBAIN, InstallerBAIN) +#endif diff --git a/libs/installer_bain/src/installerbain.h b/libs/installer_bain/src/installerbain.h new file mode 100644 index 0000000..535a31a --- /dev/null +++ b/libs/installer_bain/src/installerbain.h @@ -0,0 +1,80 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef INSTALLERBAIN_H +#define INSTALLERBAIN_H + +#include + +class InstallerBAIN : public MOBase::IPluginInstallerSimple +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.InstallerBAIN") +#endif + +public: + InstallerBAIN(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + + virtual unsigned int priority() const; + virtual bool isManualInstaller() const; + + virtual void onInstallationStart(QString const& archive, bool reinstallation, + MOBase::IModInterface* currentMod) override; + virtual void onInstallationEnd(EInstallResult result, + MOBase::IModInterface* newMod) override; + + virtual bool isArchiveSupported(std::shared_ptr tree) const; + virtual EInstallResult install(MOBase::GuessedValue& modName, + std::shared_ptr& tree, + QString& version, int& modID); + +protected: + /** + * @brief Retrieve the entries corresponding to subpackages in the given tree. + * + * @param tree The tree to check for subpackages. + * @param invalidFolders If not null, will contain the number of invalid folders. + * + * @return the list of entries corresponding to subpackages. + */ + std::vector> + findSubpackages(std::shared_ptr tree, + std::size_t* invalidFolders = nullptr) const; + +private: + const MOBase::IOrganizer* m_MOInfo; + + // Indicates if the installer was used: + bool m_InstallerUsed; + + // List of options currently installed and being installed: + QStringList m_PreviousOptions, m_SelectedOptions; +}; + +#endif // INSTALLERBAIN_H diff --git a/libs/installer_bain/vcpkg.json b/libs/installer_bain/vcpkg.json new file mode 100644 index 0000000..28051ad --- /dev/null +++ b/libs/installer_bain/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673" + } + } +} diff --git a/libs/installer_bundle/.clang-format b/libs/installer_bundle/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/installer_bundle/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/installer_bundle/.git-blame-ignore-revs b/libs/installer_bundle/.git-blame-ignore-revs new file mode 100644 index 0000000..b2ed96a --- /dev/null +++ b/libs/installer_bundle/.git-blame-ignore-revs @@ -0,0 +1 @@ +988523cac10641c510fd711d80d59c82d473f6a5 diff --git a/libs/installer_bundle/.gitattributes b/libs/installer_bundle/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/installer_bundle/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/installer_bundle/.github/workflows/build.yml b/libs/installer_bundle/.github/workflows/build.yml new file mode 100644 index 0000000..3e83565 --- /dev/null +++ b/libs/installer_bundle/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build Installer Bundle + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Installer Bundle + id: build-installer-bundle + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase diff --git a/libs/installer_bundle/.github/workflows/linting.yml b/libs/installer_bundle/.github/workflows/linting.yml new file mode 100644 index 0000000..31c504d --- /dev/null +++ b/libs/installer_bundle/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Installer Bundle Plugin + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/installer_bundle/.gitignore b/libs/installer_bundle/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/installer_bundle/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/installer_bundle/.pre-commit-config.yaml b/libs/installer_bundle/.pre-commit-config.yaml new file mode 100644 index 0000000..3103a1f --- /dev/null +++ b/libs/installer_bundle/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v19.1.5 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/installer_bundle/CMakeLists.txt b/libs/installer_bundle/CMakeLists.txt new file mode 100644 index 0000000..6c48f9d --- /dev/null +++ b/libs/installer_bundle/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(installer_bundle) + +add_subdirectory(src) diff --git a/libs/installer_bundle/CMakePresets.json b/libs/installer_bundle/CMakePresets.json new file mode 100644 index 0000000..5a63c77 --- /dev/null +++ b/libs/installer_bundle/CMakePresets.json @@ -0,0 +1,67 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/installer_bundle/src/CMakeLists.txt b/libs/installer_bundle/src/CMakeLists.txt new file mode 100644 index 0000000..7b946ce --- /dev/null +++ b/libs/installer_bundle/src/CMakeLists.txt @@ -0,0 +1,14 @@ + +cmake_minimum_required(VERSION 3.16) + +file(GLOB installer_bundle_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(installer_bundle SHARED ${installer_bundle_SOURCES}) +mo2_configure_plugin(installer_bundle NO_SOURCES WARNINGS OFF) +target_link_libraries(installer_bundle PRIVATE mo2::uibase) +mo2_install_plugin(installer_bundle) diff --git a/libs/installer_bundle/src/installer_bundle_en.ts b/libs/installer_bundle/src/installer_bundle_en.ts new file mode 100644 index 0000000..d38444a --- /dev/null +++ b/libs/installer_bundle/src/installer_bundle_en.ts @@ -0,0 +1,46 @@ + + + + + InstallerBundle + + + Bundle Installer + + + + + Proxy-Installer to handle bundles containing the actual mod archive + + + + + MultiArchiveDialog + + + Bundled archives + + + + + This archive contains multiple archives that can be installed separately. Please select the archive you want to extract and install. + + + + + + Opens a Dialog that allows custom modifications. + + + + + Manual + + + + + Cancel + + + + diff --git a/libs/installer_bundle/src/installerbundle.cpp b/libs/installer_bundle/src/installerbundle.cpp new file mode 100644 index 0000000..65ea02f --- /dev/null +++ b/libs/installer_bundle/src/installerbundle.cpp @@ -0,0 +1,194 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "installerbundle.h" + +#include + +#include + +#include "multiarchivedialog.h" + +using namespace MOBase; + +InstallerBundle::InstallerBundle() {} + +bool InstallerBundle::init(IOrganizer* organizer) +{ + m_Organizer = organizer; + return true; +} + +QString InstallerBundle::name() const +{ + return "Bundle Installer"; +} + +QString InstallerBundle::localizedName() const +{ + return tr("Bundle Installer"); +} + +QString InstallerBundle::author() const +{ + return "Tannin"; +} + +QString InstallerBundle::description() const +{ + return tr("Proxy-Installer to handle bundles containing the actual mod archive"); +} + +VersionInfo InstallerBundle::version() const +{ + return VersionInfo(1, 1, 0, VersionInfo::RELEASE_FINAL); +} + +QList InstallerBundle::settings() const +{ + return {PluginSetting("auto_reinstall", + "when reinstalling from an archive containing multiple mods, " + "automatically select the previously installed", + true)}; +} + +unsigned int InstallerBundle::priority() const +{ + return 10; +} + +bool InstallerBundle::isManualInstaller() const +{ + return false; +} + +void InstallerBundle::onInstallationStart(QString const& archive, bool reinstallation, + IModInterface* currentMod) +{ + // We reset some field and fetch the previously installed file: + m_InstallationFile = archive; + m_InstallerUsed = false; + m_SelectedFile = ""; + m_PreviousFile = ""; + + if (reinstallation && m_Organizer->pluginSetting(name(), "auto_reinstall").toBool()) { + m_PreviousFile = currentMod->pluginSetting(name(), "archive", QString()).toString(); + } +} +void InstallerBundle::onInstallationEnd(EInstallResult result, IModInterface* newMod) +{ + if (result == EInstallResult::RESULT_SUCCESS && m_InstallerUsed) { + newMod->setInstallationFile(m_InstallationFile); + if (!m_SelectedFile.isEmpty()) { + newMod->setPluginSetting(name(), "archive", m_SelectedFile); + } + } +} + +std::vector> +InstallerBundle::findObjects(std::shared_ptr tree) const +{ + std::vector> entries; + // Check if we have an archive: + auto managerExtensions = manager()->getSupportedExtensions(); + + // Look for a file with a valid extension: + int nDirs = 0; + for (auto entry : *tree) { + if (entry->isFile()) { + if (managerExtensions.contains(entry->suffix(), + FileNameComparator::CaseSensitivity)) { + entries.push_back(entry); + } + } else { + nDirs++; + } + } + + if (!entries.empty()) { + return entries; + } + + // Arguably this should also check the name of the directory we're looking at + // is the same as the module name: + if (nDirs != 1) { + return {}; + } + + // Retrieve the first folder which is the first item: + return findObjects(tree->at(0)->astree()); +} + +bool InstallerBundle::isArchiveSupported(std::shared_ptr tree) const +{ + return !findObjects(tree).empty(); +} + +IPluginInstaller::EInstallResult +InstallerBundle::install(GuessedValue& modName, + std::shared_ptr& tree, QString&, int& modId) +{ + // Find a valid "object": + auto entries = findObjects(tree); + if (entries.empty()) { + return IPluginInstaller::RESULT_NOTATTEMPTED; + } + + std::shared_ptr entry = nullptr; + if (entries.size() == 1) { + entry = entries[0]; + } else { + if (!m_PreviousFile.isEmpty()) { + entry = tree->find(m_PreviousFile); + } + + if (entry == nullptr) { + MultiArchiveDialog dialog(entries, parentWidget()); + if (dialog.exec() == QDialog::Accepted) { + entry = dialog.selectedEntry(); + m_SelectedFile = entry->pathFrom(tree); + } else { + if (dialog.manualRequested()) { + return IPluginInstaller::RESULT_MANUALREQUESTED; + } else { + return IPluginInstaller::RESULT_CANCELED; + } + } + } + } + + if (entry == nullptr) { + return IPluginInstaller::RESULT_CANCELED; + } + + m_InstallerUsed = true; + + // Extract it: + QString tempFile = manager()->extractFile(entry); + IPluginInstaller::EInstallResult res = + manager()->installArchive(modName, tempFile, modId); + if (res == IPluginInstaller::RESULT_SUCCESS) { + res = IPluginInstaller::RESULT_SUCCESSCANCEL; + } + return res; +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(installerBundle, InstallerBundle) +#endif diff --git a/libs/installer_bundle/src/installerbundle.h b/libs/installer_bundle/src/installerbundle.h new file mode 100644 index 0000000..59cc28c --- /dev/null +++ b/libs/installer_bundle/src/installerbundle.h @@ -0,0 +1,88 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef INSTALLERBUNDLE_H +#define INSTALLERBUNDLE_H + +#include + +class InstallerBundle : public MOBase::IPluginInstallerSimple +{ + + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.InstallerBundle") +#endif + +public: + InstallerBundle(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + + virtual unsigned int priority() const override; + virtual bool isManualInstaller() const override; + + virtual void onInstallationStart(QString const& archive, bool reinstallation, + MOBase::IModInterface* currentMod) override; + virtual void onInstallationEnd(EInstallResult result, + MOBase::IModInterface* newMod) override; + + virtual bool + isArchiveSupported(std::shared_ptr tree) const override; + virtual EInstallResult install(MOBase::GuessedValue& modName, + std::shared_ptr& tree, + QString& version, int& modID) override; + +private: + /** + * @brief Find the entries that can be extracted from this archive. + * + * @param tree The tree to look the entry in. + * + * @return the entry, if one was found, or a null pointer. + */ + std::vector> + findObjects(std::shared_ptr tree) const; + +private: + MOBase::IOrganizer* m_Organizer; + + // The archive being installed: + QString m_InstallationFile; + + // Indicates if this installer was used during the installation process: + bool m_InstallerUsed; + + // Contains the file installed (when multiple archives are present), or an + // empty string: + QString m_SelectedFile; + + // Contains the file previously installed (when multiple archives are present), or an + // empty string: + QString m_PreviousFile; +}; + +#endif // INSTALLERBUNDLE_H diff --git a/libs/installer_bundle/src/multiarchivedialog.cpp b/libs/installer_bundle/src/multiarchivedialog.cpp new file mode 100644 index 0000000..03774f6 --- /dev/null +++ b/libs/installer_bundle/src/multiarchivedialog.cpp @@ -0,0 +1,40 @@ +#include "multiarchivedialog.h" +#include "ui_multiarchivedialog.h" + +using namespace MOBase; + +MultiArchiveDialog::MultiArchiveDialog( + std::vector> entries, QWidget* parent) + : QDialog(parent), ui(new Ui::MultiArchiveDialog), m_Manual(false), + m_SelectedEntry(nullptr) +{ + ui->setupUi(this); + + auto layout = ui->middleLayout; + for (auto& entry : entries) { + QPushButton* button = new QPushButton(entry->name(), this); + button->setStyleSheet("text-align: left; padding: 10px; font-weight: bold;"); + layout->addWidget(button); + + connect(button, &QPushButton::clicked, [this, entry]() { + m_SelectedEntry = entry; + this->accept(); + }); + } +} + +MultiArchiveDialog::~MultiArchiveDialog() +{ + delete ui; +} + +void MultiArchiveDialog::on_cancelBtn_clicked() +{ + this->reject(); +} + +void MultiArchiveDialog::on_manualBtn_clicked() +{ + m_Manual = true; + this->reject(); +} diff --git a/libs/installer_bundle/src/multiarchivedialog.h b/libs/installer_bundle/src/multiarchivedialog.h new file mode 100644 index 0000000..39949b9 --- /dev/null +++ b/libs/installer_bundle/src/multiarchivedialog.h @@ -0,0 +1,78 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef BAINCOMPLEXINSTALLERDIALOG_H +#define BAINCOMPLEXINSTALLERDIALOG_H + +#include + +#include + +#include + +namespace Ui +{ +class MultiArchiveDialog; +} + +/** + * + **/ +class MultiArchiveDialog : public QDialog +{ + Q_OBJECT + +public: + /** + * @brief Constructor + * + * @param tree the directory tree of the archive. The caller is resonsible to verify + *this actually qualifies as a bain installer + * @param modName proposed name for the mod. The dialog allows the user to change this + * @param packageTXT path to the extracted package.txt file or an empty string if + *there is none + * @param parent parent widget + **/ + explicit MultiArchiveDialog( + std::vector> entries, + QWidget* parent); + ~MultiArchiveDialog(); + + /** + * @return bool true if the user requested the manual dialog + **/ + bool manualRequested() const { return m_Manual; } + + /** + * @return QString the (user-modified) name to be used for the mod + **/ + auto selectedEntry() const { return m_SelectedEntry; } + +private slots: + + void on_manualBtn_clicked(); + void on_cancelBtn_clicked(); + +private: + Ui::MultiArchiveDialog* ui; + bool m_Manual; + std::shared_ptr m_SelectedEntry; +}; + +#endif // BAINCOMPLEXINSTALLERDIALOG_H diff --git a/libs/installer_bundle/src/multiarchivedialog.ui b/libs/installer_bundle/src/multiarchivedialog.ui new file mode 100644 index 0000000..47d5d8b --- /dev/null +++ b/libs/installer_bundle/src/multiarchivedialog.ui @@ -0,0 +1,77 @@ + + + MultiArchiveDialog + + + + 0 + 0 + 424 + 146 + + + + Bundled archives + + + + + + This archive contains multiple archives that can be installed separately. Please select the archive you want to extract and install. + + + true + + + + + + + + + + + + Opens a Dialog that allows custom modifications. + + + Opens a Dialog that allows custom modifications. + + + Manual + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + false + + + + + + + + + + diff --git a/libs/installer_bundle/vcpkg.json b/libs/installer_bundle/vcpkg.json new file mode 100644 index 0000000..28051ad --- /dev/null +++ b/libs/installer_bundle/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673" + } + } +} diff --git a/libs/installer_fomod/.clang-format b/libs/installer_fomod/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/installer_fomod/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/installer_fomod/.git-blame-ignore-revs b/libs/installer_fomod/.git-blame-ignore-revs new file mode 100644 index 0000000..9614dd1 --- /dev/null +++ b/libs/installer_fomod/.git-blame-ignore-revs @@ -0,0 +1 @@ +217a27ec63f5f50c6559864394b106abd8accbe1 diff --git a/libs/installer_fomod/.gitattributes b/libs/installer_fomod/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/installer_fomod/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/installer_fomod/.github/workflows/build.yml b/libs/installer_fomod/.github/workflows/build.yml new file mode 100644 index 0000000..e3565de --- /dev/null +++ b/libs/installer_fomod/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build Installer FOMOD + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }} + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Installer FOMOD + id: build-installer-fomod + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase diff --git a/libs/installer_fomod/.github/workflows/linting.yml b/libs/installer_fomod/.github/workflows/linting.yml new file mode 100644 index 0000000..c199ade --- /dev/null +++ b/libs/installer_fomod/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Installer FOMOD Plugin + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/installer_fomod/.gitignore b/libs/installer_fomod/.gitignore new file mode 100644 index 0000000..b6807b5 --- /dev/null +++ b/libs/installer_fomod/.gitignore @@ -0,0 +1,5 @@ +edit +*.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/installer_fomod/.pre-commit-config.yaml b/libs/installer_fomod/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/installer_fomod/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/installer_fomod/CMakeLists.txt b/libs/installer_fomod/CMakeLists.txt new file mode 100644 index 0000000..7510c74 --- /dev/null +++ b/libs/installer_fomod/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.16) + +project(installer_fomod) + +find_package(Qt6 COMPONENTS Widgets REQUIRED) + +# Compatibility helpers for this monorepo build (upstream expects mo2-cmake package). +if(NOT COMMAND mo2_configure_plugin) + function(mo2_configure_plugin target) + set_target_properties(${target} PROPERTIES + AUTOMOC ON + AUTOUIC ON + AUTORCC ON + CXX_STANDARD 23 + CXX_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON + ) + + target_compile_options(${target} PRIVATE + "-idirafter" "${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" + ) + set_property(TARGET ${target} APPEND PROPERTY AUTOMOC_MOC_OPTIONS + "-I${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" + ) + endfunction() +endif() + +if(NOT COMMAND mo2_install_plugin) + function(mo2_install_plugin target) + install(TARGETS ${target} + LIBRARY DESTINATION plugins + ) + endfunction() +endif() + +add_subdirectory(src) diff --git a/libs/installer_fomod/CMakePresets.json b/libs/installer_fomod/CMakePresets.json new file mode 100644 index 0000000..5a63c77 --- /dev/null +++ b/libs/installer_fomod/CMakePresets.json @@ -0,0 +1,67 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/installer_fomod/src/CMakeLists.txt b/libs/installer_fomod/src/CMakeLists.txt new file mode 100644 index 0000000..cbb324d --- /dev/null +++ b/libs/installer_fomod/src/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB INSTALLER_FOMOD_SOURCES + CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(installer_fomod SHARED ${INSTALLER_FOMOD_SOURCES}) +mo2_configure_plugin(installer_fomod WARNINGS OFF) +target_include_directories(installer_fomod PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) +target_link_libraries(installer_fomod PRIVATE mo2::uibase Qt6::Widgets) +mo2_install_plugin(installer_fomod) diff --git a/libs/installer_fomod/src/fomodinstallerdialog.cpp b/libs/installer_fomod/src/fomodinstallerdialog.cpp new file mode 100644 index 0000000..ef6c44f --- /dev/null +++ b/libs/installer_fomod/src/fomodinstallerdialog.cpp @@ -0,0 +1,1701 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "fomodinstallerdialog.h" +#include "ui_fomodinstallerdialog.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fomodscreenshotdialog.h" +#include "xmlreader.h" + +using namespace MOBase; + +bool ControlsAscending(QAbstractButton* LHS, QAbstractButton* RHS) +{ + return LHS->text() < RHS->text(); +} + +bool ControlsDescending(QAbstractButton* LHS, QAbstractButton* RHS) +{ + return LHS->text() > RHS->text(); +} + +bool PagesAscending(QGroupBox* LHS, QGroupBox* RHS) +{ + return LHS->title() < RHS->title(); +} + +bool PagesDescending(QGroupBox* LHS, QGroupBox* RHS) +{ + return LHS->title() > RHS->title(); +} + +FomodInstallerDialog::FomodInstallerDialog( + InstallerFomod* installer, const GuessedValue& modName, + const QString& fomodPath, + const std::function& fileCheck, + QWidget* parent) + : QDialog(parent), ui(new Ui::FomodInstallerDialog), m_Installer(installer), + m_ModName(modName), m_ModID(-1), m_FomodPath(fomodPath), m_Manual(false), + m_FileCheck(fileCheck), m_FileSystemItemSequence() +{ + ui->setupUi(this); + setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint); + setWindowTitle(modName); + + updateNameEdit(); + ui->nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); +} + +FomodInstallerDialog::~FomodInstallerDialog() +{ + delete ui; +} + +bool FomodInstallerDialog::hasOptions() +{ + return ui->stepsStack->count() > 0; +} + +void FomodInstallerDialog::transformToSmallInstall() +{ + ui->descriptionText->setVisible(false); + ui->screenshotLabel->setVisible(false); + ui->stepsStack->setVisible(false); + adjustSize(); +} + +void FomodInstallerDialog::updateNameEdit() +{ + ui->nameCombo->clear(); + for (auto iter = m_ModName.variants().begin(); iter != m_ModName.variants().end(); + ++iter) { + ui->nameCombo->addItem(*iter); + } + + ui->nameCombo->setCurrentIndex(ui->nameCombo->findText(m_ModName)); +} + +int FomodInstallerDialog::bomOffset(const QByteArray& buffer) +{ + static const unsigned char BOM_UTF8[] = {0xEF, 0xBB, 0xBF}; + static const unsigned char BOM_UTF16BE[] = {0xFE, 0xFF}; + static const unsigned char BOM_UTF16LE[] = {0xFF, 0xFE}; + + if (buffer.startsWith(reinterpret_cast(BOM_UTF8))) + return 3; + if (buffer.startsWith(reinterpret_cast(BOM_UTF16BE)) || + buffer.startsWith(reinterpret_cast(BOM_UTF16LE))) + return 2; + + return 0; +} + +struct XmlParseError : std::runtime_error +{ + XmlParseError(const QString& message) : std::runtime_error(qUtf8Printable(message)) {} +}; + +QByteArray skipXmlHeader(QIODevice& file) +{ + static const unsigned char UTF16LE_BOM[] = {0xFF, 0xFE}; + static const unsigned char UTF16BE_BOM[] = {0xFE, 0xFF}; + static const unsigned char UTF8_BOM[] = {0xEF, 0xBB, 0xBF}; + static const unsigned char UTF16LE[] = {0x3C, 0x00, 0x3F, 0x00}; + static const unsigned char UTF16BE[] = {0x00, 0x3C, 0x00, 0x3F}; + static const unsigned char UTF8[] = {0x3C, 0x3F, 0x78, 0x6D}; + + file.seek(0); + QByteArray rawBytes = file.read(4); + QTextStream stream(&file); + int bom = 0; + if (rawBytes.startsWith((const char*)UTF16LE_BOM)) { + stream.setEncoding(QStringConverter::Encoding::Utf16LE); + bom = 2; + } else if (rawBytes.startsWith((const char*)UTF16BE_BOM)) { + stream.setEncoding(QStringConverter::Encoding::Utf16BE); + bom = 2; + } else if (rawBytes.startsWith((const char*)UTF8_BOM)) { + stream.setEncoding(QStringConverter::Encoding::Utf8); + bom = 3; + } else if (rawBytes.startsWith(QByteArray((const char*)UTF16LE, 4))) { + stream.setEncoding(QStringConverter::Encoding::Utf16LE); + } else if (rawBytes.startsWith(QByteArray((const char*)UTF16BE, 4))) { + stream.setEncoding(QStringConverter::Encoding::Utf16BE); + } else if (rawBytes.startsWith(QByteArray((const char*)UTF8, 4))) { + stream.setEncoding(QStringConverter::Encoding::Utf8); + } // otherwise maybe the textstream knows the encoding? + + stream.seek(bom); + QString header = stream.readLine(); + if (!header.startsWith(" encodings{ + QStringConverter::Encoding::Utf16, QStringConverter::Encoding::Utf8, + QStringConverter::Encoding::Latin1}; + + bool success = false; + std::string errorMessage; + try { + XmlReader reader(&file); + (this->*callback)(reader); + success = true; + } catch (const XmlParseError& e) { + log::warn("The {} in this file is incorrectly encoded ({}). Applying heuristics...", + file.fileName(), e.what()); + } + + if (!success) { + // nmm's xml parser is less strict than the one from qt and allows files with + // wrong encoding in the header. Being strict here would be bad user experience + // this works around bad headers. + QByteArray headerlessData = skipXmlHeader(file); + + // try parsing the file with several encodings to support broken files + for (auto encoding : encodings) { + log::debug("Trying encoding {} for {}... ", encoding, file.fileName()); + try { + QStringEncoder encoder(encoding); + XmlReader reader( + encoder.encode(QString("") + .arg(encoder.name())) + + headerlessData); + (this->*callback)(reader); + log::debug("Interpreting {} as {}.", file.fileName(), encoding); + success = true; + break; + } catch (const XmlParseError& e) { + log::debug("Not {}: {}.", encoding, e.what()); + } + } + if (!success) { + reportError( + tr("Failed to parse %1. See console for details.").arg(file.fileName())); + } + + file.close(); + } +} + +void FomodInstallerDialog::readInfoXml() +{ + QFile file(QDir::tempPath() + "/" + m_FomodPath + "/fomod/info.xml"); + + // We don't need a info.xml file, so we just return if we cannot open it: + if (!file.open(QIODevice::ReadOnly)) { + return; + } + readXml(file, &FomodInstallerDialog::parseInfo); +} + +void FomodInstallerDialog::readModuleConfigXml() +{ + QFile file(QDir::tempPath() + "/" + m_FomodPath + "/fomod/ModuleConfig.xml"); + if (!file.open(QIODevice::ReadOnly)) { + throw Exception(tr("%1 missing.").arg(file.fileName())); + } + readXml(file, &FomodInstallerDialog::parseModuleConfig); +} + +void FomodInstallerDialog::initData(IOrganizer* moInfo) +{ + m_MoInfo = moInfo; + + // parse provided package information + readInfoXml(); + + QString screenshotPath = + QDir::tempPath() + "/" + m_FomodPath + "/fomod/screenshot.png"; + if (!QImage(screenshotPath).isNull()) { + ui->screenshotLabel->setScalableResource(screenshotPath); + ui->screenshotExpand->setVisible(false); + } + + readModuleConfigXml(); +} + +QString FomodInstallerDialog::getName() const +{ + return ui->nameCombo->currentText(); +} + +QString FomodInstallerDialog::getVersion() const +{ + return ui->versionLabel->text(); +} + +int FomodInstallerDialog::getModID() const +{ + return m_ModID; +} + +QString FomodInstallerDialog::getURL() const +{ + return m_URL; +} + +void FomodInstallerDialog::applyPriority(Leaves& leaves, IFileTree const* tree, + int priority) +{ + for (auto entry : *tree) { + if (entry->isDir()) { + applyPriority(leaves, entry->astree().get(), priority); + } else { + leaves.insert({entry.get(), {priority, entry->path()}}); + } + } +} + +void FomodInstallerDialog::copyLeaf(std::shared_ptr sourceEntry, + std::shared_ptr destinationTree, + QString destinationPath, + IFileTree::OverwritesType& overwrites, + Leaves& leaves, int pri) +{ + // TODO: + applyPriority(leaves, sourceEntry->parent().get(), pri); + + if (destinationPath.isEmpty() || destinationPath.endsWith("/") || + destinationPath.endsWith("\\")) { + destinationPath += sourceEntry->name(); + } + + auto oldEntry = destinationTree->find(destinationPath); + if (oldEntry != nullptr) { + overwrites[oldEntry] = sourceEntry; + } + + destinationTree->copy(sourceEntry, destinationPath, IFileTree::InsertPolicy::REPLACE); +} + +bool FomodInstallerDialog::copyFileIterator(std::shared_ptr sourceTree, + std::shared_ptr destinationTree, + const FileDescriptor* descriptor, + Leaves& leaves, + IFileTree::OverwritesType& overwrites) +{ + QString source = (m_FomodPath.length() != 0) + ? QDir(m_FomodPath).filePath(descriptor->m_Source) + : descriptor->m_Source; + int pri = descriptor->m_Priority; + QString destination = descriptor->m_Destination; + + if (descriptor->m_IsFolder) { + std::shared_ptr sourceNode = sourceTree->findDirectory(source); + + if (sourceNode == nullptr) { + log::error("Folder '{}' not found.", source); + return false; + } + + // Apply the priority on the source tree: + applyPriority(leaves, sourceNode.get(), pri); + + // addDirectory will create the directory if it does not exist: + std::shared_ptr targetNode = destinationTree->addDirectory(destination); + + // Note (Holt59): Before, the directories were processed before the files, and the + // files were processed in reverse order. The directories before files was mandatory + // since both were stored differently, but I have no idea why the files were + // processed in reverse order and it does not make sense since there cannot be two + // identical file in a tree. Also, the files were copied but the directories were + // moved, I am pretty sure this made no sense. + for (auto e : *sourceNode) { + targetNode->copy(e, "", IFileTree::InsertPolicy::MERGE); + } + + } else { + std::shared_ptr sourceEntry = sourceTree->find(source); + + if (sourceEntry == nullptr) { + log::error("File '{}' not found.", source); + return false; + } + + copyLeaf(sourceEntry, destinationTree, destination, overwrites, leaves, pri); + } + return true; +} + +std::pair +FomodInstallerDialog::testCondition(int maxIndex, + const ValueCondition* valCondition) const +{ + return testCondition(maxIndex, valCondition->m_Name, valCondition->m_Value); +} + +std::pair +FomodInstallerDialog::testCondition(int maxIndex, + const ConditionFlag* conditionFlag) const +{ + return testCondition(maxIndex, conditionFlag->m_Name, conditionFlag->m_Value); +} + +std::pair +FomodInstallerDialog::testCondition(int maxIndex, const SubCondition* condition) const +{ + ConditionOperator op = condition->m_Operator; + for (const Condition* cond : condition->m_Conditions) { + std::pair conditionMatches = cond->test(maxIndex, this); + if (!conditionMatches.first) + qWarning() << conditionMatches.second; + if (op == OP_OR && conditionMatches.first) { + return std::make_pair( + true, tr("At least one condition was successful in an 'OR' clause!")); + } + if (op == OP_AND && !conditionMatches.first) { + return conditionMatches; + } + } + // If we get through here, everything matched (AND) or nothing matched (OR) + if (op == OP_AND) + return std::make_pair( + true, tr("All conditions were successful in an 'AND' clause!")); + else + return std::make_pair( + false, tr("No conditions were successful in an 'OR' clause!")); +} + +QString FomodInstallerDialog::toString(IPluginList::PluginStates state) +{ + if (state.testFlag(IPluginList::STATE_MISSING)) + return "Missing"; + if (state.testFlag(IPluginList::STATE_INACTIVE)) + return "Inactive"; + if (state.testFlag(IPluginList::STATE_ACTIVE)) + return "Active"; + throw Exception(tr("invalid plugin state %1").arg(static_cast(state))); +} + +std::pair +FomodInstallerDialog::testCondition(int, const FileCondition* condition) const +{ + static const std::map trPluginStates = { + {"Missing", tr("Missing")}, + {"Inactive", tr("Inactive")}, + {"Active", tr("Active")}}; + + QString result = toString(m_FileCheck(condition->m_File)); + if (result == condition->m_State) + return std::make_pair( + true, tr("Success: The file '%1' was marked %2.") + .arg(condition->m_File) + .arg(trPluginStates.at(condition->m_State).toLower())); + else + return std::make_pair( + false, tr("Missing requirement: The file '%1' should be %2, but was %3!") + .arg(condition->m_File) + .arg(trPluginStates.at(condition->m_State).toLower()) + .arg(trPluginStates.at(result).toLower())); +} + +namespace FOMOD +{ +class Version +{ +public: + explicit Version(QString const& v); + + friend bool operator<=(Version const&, Version const&); + +private: + std::array m_version; +}; + +Version::Version(QString const& v) +{ + std::istringstream parser(v.toStdString()); + m_version.fill(0); + parser >> m_version[0]; + for (int idx = 1; idx < 4; idx++) { + parser.get(); // Skip period + parser >> m_version[idx]; + } +} + +bool operator<=(Version const& lhs, Version const& rhs) +{ + return lhs.m_version <= rhs.m_version; +} + +} // namespace FOMOD + +std::pair +FomodInstallerDialog::testCondition(int, const VersionCondition* condition) const +{ + QString version; + MOBase::IPluginGame const* game = m_MoInfo->managedGame(); + + QString typeName; + switch (condition->m_Type) { + case VersionCondition::v_Game: { + version = game->gameVersion(); + typeName = game->gameName(); + } break; + + case VersionCondition::v_FOMM: + // We should use m_MoInfo->appVersion() but then we wouldn't be able to + // install anything as MO is at 0.3.11 at the time of writing. + version = "0.13.21"; + typeName = "FOMM (FOMOD syntax)"; + break; + + case VersionCondition::v_FOSE: { + auto extender = m_MoInfo->gameFeatures()->gameFeature(); + if (extender != nullptr) { + version = extender->getExtenderVersion(); + typeName = extender->BinaryName(); + } else { + version = "not installed"; + typeName = "the script extender"; + } + } break; + } + if (FOMOD::Version(condition->m_RequiredVersion) <= FOMOD::Version(version)) + return std::make_pair( + true, tr("Success: The required version of %1 is %2, and was detected as %3.") + .arg(typeName) + .arg(condition->m_RequiredVersion) + .arg(version)); + else + return std::make_pair(false, + tr("Missing requirement: The required version " + "of %1 is %2, but was detected as %3.") + .arg(typeName) + .arg(condition->m_RequiredVersion) + .arg(version)); +} + +bool FomodInstallerDialog::displayMissingFilesDialog( + std::vector missingFiles) +{ + QMessageBox dialog(parentWidget()); + + dialog.setIcon(QMessageBox::Icon::Warning); + dialog.setWindowTitle(tr("Missing files or folders")); + dialog.addButton(tr("Install anyway"), QMessageBox::AcceptRole); + dialog.addButton(tr("Cancel"), QMessageBox::RejectRole); + + QString text = tr("The following files or folders were not found in the archive. " + "This is likely due to an incorrect FOMOD installer. " + "This mod may not work properly."); + text.append("\n\n"); + for (auto* fileDescriptor : missingFiles) { + QString temp = fileDescriptor->m_IsFolder ? tr("Folder '%1'.") : tr("File '%1'."); + text.append("- " + temp.arg(fileDescriptor->m_Source) + "\n"); + } + + dialog.setTextFormat(Qt::MarkdownText); + dialog.setText(text); + + return dialog.exec() == QMessageBox::AcceptRole; +} + +IPluginInstaller::EInstallResult +FomodInstallerDialog::updateTree(std::shared_ptr& tree) +{ + FileDescriptorList descriptorList; + + // enable all required files + for (FileDescriptor* file : m_RequiredFiles) { + descriptorList.push_back(file); + } + + // enable all conditional file installs (files programatically selected by conditions + // instead of a user selection. usually dependencies) + for (ConditionalInstall& cond : m_ConditionalInstalls) { + SubCondition* condition = &cond.m_Condition; + std::pair result = condition->test(ui->stepsStack->count(), this); + if (result.first) { + for (FileDescriptor* file : cond.m_Files) { + descriptorList.push_back(file); + } + } + } + + // enable all user-enabled choices + for (int i = 0; i < ui->stepsStack->count(); ++i) { + if (testVisible(i)) { + QList choices = + ui->stepsStack->widget(i)->findChildren("choice"); + for (QAbstractButton* choice : choices) { + if (choice->isChecked()) { + QVariantList fileList = choice->property("files").toList(); + for (QVariant fileVariant : fileList) { + descriptorList.push_back(fileVariant.value()); + } + } + } + } + } + + std::stable_sort(descriptorList.begin(), descriptorList.end(), byPriority); + + IFileTree::OverwritesType overwrites; + Leaves leaves; + std::shared_ptr newTree = tree->createOrphanTree(); + + std::vector failures; + + const QStringList ignoreMissingFolder = + m_MoInfo + ->persistent(m_Installer->name(), "ignored_missing_files", + QStringList{"no folder"}) + .toStringList(); + + for (const FileDescriptor* file : descriptorList) { + if (!copyFileIterator(tree, newTree, file, leaves, overwrites)) { + if (!ignoreMissingFolder.contains(file->m_Source, + FileNameComparator::CaseSensitivity)) { + failures.push_back(file); + } + } + } + + if (!failures.empty()) { + if (!displayMissingFilesDialog(failures)) { + return IPluginInstaller::RESULT_CANCELED; + } + } + + for (auto overwrite : overwrites) { + if (leaves[overwrite.first.get()].priority == + leaves[overwrite.second.get()].priority) { + qWarning() << "Overriding " << leaves[overwrite.first.get()].path << " with " + << leaves[overwrite.second.get()].path + << " which has the same priority"; + } + } + + // Update the tree: + tree = newTree; + + return IPluginInstaller::RESULT_SUCCESS; +} + +void FomodInstallerDialog::highlightControl(QAbstractButton* button) +{ + QVariant screenshotName = button->property("screenshot"); + if (screenshotName.isValid()) { + QString screenshotFileName = screenshotName.toString(); + if (!screenshotFileName.isEmpty()) { + QString temp = QDir::tempPath() + "/" + m_FomodPath + "/" + + QDir::fromNativeSeparators(screenshotFileName); + ui->screenshotLabel->setScalableResource(temp); + ui->screenshotExpand->setVisible(true); + } else { + ui->screenshotLabel->setScalableResource(QString()); + ui->screenshotExpand->setVisible(false); + } + } + ui->descriptionText->setText(button->property("description").toString()); +} + +bool FomodInstallerDialog::eventFilter(QObject* object, QEvent* event) +{ + QAbstractButton* button = qobject_cast(object); + if ((button != nullptr) && (event->type() == QEvent::HoverEnter)) { + highlightControl(button); + } + return QDialog::eventFilter(object, event); +} + +QString FomodInstallerDialog::readContent(QXmlStreamReader& reader) +{ + if (reader.readNext() == XmlReader::Characters) { + return reader.text().toString(); + } else { + return QString(); + } +} + +void FomodInstallerDialog::parseInfo(XmlReader& reader) +{ + while (!reader.atEnd()) { + switch (reader.readNext()) { + case QXmlStreamReader::StartElement: { + if (reader.name().toString() == "Name") { + m_ModName.update(readContent(reader), GUESS_META); + updateNameEdit(); + } else if (reader.name().toString() == "Author") { + ui->authorLabel->setText(readContent(reader)); + } else if (reader.name().toString() == "Version") { + ui->versionLabel->setText(readContent(reader)); + } else if (reader.name().toString() == "Id") { + m_ModID = readContent(reader).toInt(); + } else if (reader.name().toString() == "Website") { + m_URL = readContent(reader); + ui->websiteLabel->setText(tr("Link").arg(m_URL)); + ui->websiteLabel->setToolTip(m_URL); + } + } break; + default: { + } break; + } + } + if (reader.hasError()) { + throw XmlParseError( + QString("%1 in line %2").arg(reader.errorString()).arg(reader.lineNumber())); + } +} + +FomodInstallerDialog::ItemOrder +FomodInstallerDialog::getItemOrder(const QString& orderString) +{ + if (orderString == "Ascending") { + return ORDER_ASCENDING; + } else if (orderString == "Descending") { + return ORDER_DESCENDING; + } else if (orderString == "Explicit") { + return ORDER_EXPLICIT; + } else { + throw Exception(tr("unsupported order type %1").arg(orderString)); + } +} + +FomodInstallerDialog::GroupType +FomodInstallerDialog::getGroupType(const QString& typeString) +{ + if (typeString == "SelectAtLeastOne") { + return TYPE_SELECTATLEASTONE; + } else if (typeString == "SelectAtMostOne") { + return TYPE_SELECTATMOSTONE; + } else if (typeString == "SelectExactlyOne") { + return TYPE_SELECTEXACTLYONE; + } else if (typeString == "SelectAny") { + return TYPE_SELECTANY; + } else if (typeString == "SelectAll") { + return TYPE_SELECTALL; + } else { + throw Exception(tr("unsupported group type %1").arg(typeString)); + } +} + +FomodInstallerDialog::PluginType +FomodInstallerDialog::getPluginType(const QString& typeString) +{ + if (typeString == "Required") { + return FomodInstallerDialog::TYPE_REQUIRED; + } else if (typeString == "Optional") { + return FomodInstallerDialog::TYPE_OPTIONAL; + } else if (typeString == "Recommended") { + return FomodInstallerDialog::TYPE_RECOMMENDED; + } else if (typeString == "NotUsable") { + return FomodInstallerDialog::TYPE_NOTUSABLE; + } else if (typeString == "CouldBeUsable") { + return FomodInstallerDialog::TYPE_COULDBEUSABLE; + } else { + qCritical("invalid plugin type %s", qUtf8Printable(typeString)); + return FomodInstallerDialog::TYPE_OPTIONAL; + } +} + +void FomodInstallerDialog::readFileList(XmlReader& reader, FileDescriptorList& fileList) +{ + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "folder" || reader.name().toString() == "file") { + QXmlStreamAttributes attributes = reader.attributes(); + // This is a horrendous hack. It doesn't make sense to specify an empty source + // folder name, as it would require you to copy everything including the fomod + // directory. However, people have been known to write entries like in order to achieve an option that does nothing. Are + // groups and buttons that hard? An empty source file is very probably a serious + // error but given people do the above, I'm assuming that they probably assume + // will work the same, so I'm not + // differentiating. Similarly, I'm not checking for the destination if the source + // is blank. Why'd you want to copy the fomod directory on an install? + if (attributes.value("source").isEmpty()) { + log::debug("Ignoring {} entry with empty source.", reader.name().toString()); + } else { + FileDescriptor* file = new FileDescriptor(this); + file->m_Source = attributes.value("source").toString(); + file->m_Destination = attributes.hasAttribute("destination") + ? attributes.value("destination").toString() + : file->m_Source; + file->m_Priority = attributes.hasAttribute("priority") + ? attributes.value("priority").toString().toInt() + : 0; + file->m_FileSystemItemSequence = ++m_FileSystemItemSequence; + file->m_IsFolder = reader.name().toString() == "folder"; + file->m_InstallIfUsable = + attributes.hasAttribute("installIfUsable") + ? (attributes.value("installIfUsable").compare((QString) "true") == 0) + : false; + file->m_AlwaysInstall = + attributes.hasAttribute("alwaysInstall") + ? (attributes.value("alwaysInstall").compare((QString) "true") == 0) + : false; + + fileList.push_back(file); + } + reader.finishedElement(); + } else { + reader.unexpected(); + } + } +} + +void FomodInstallerDialog::readDependencyPattern(XmlReader& reader, + DependencyPattern& pattern) +{ + // sequence + // dependency + // type + QString self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "dependencies") { + readCompositeDependency(reader, pattern.condition); + } else if (reader.name().toString() == "type") { + pattern.type = getPluginType(reader.attributes().value("name").toString()); + reader.finishedElement(); + } else { + reader.unexpected(); + } + } +} + +void FomodInstallerDialog::readDependencyPatternList(XmlReader& reader, + DependencyPatternList& patterns) +{ + QString self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "pattern") { + DependencyPattern pattern; + readDependencyPattern(reader, pattern); + patterns.push_back(pattern); + } else { + reader.unexpected(); + } + } +} + +void FomodInstallerDialog::readDependencyPluginType(XmlReader& reader, + PluginTypeInfo& info) +{ + // sequence + // defaultType + // patterns + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "defaultType") { + info.m_DefaultType = getPluginType(reader.attributes().value("name").toString()); + reader.finishedElement(); + } else if (reader.name().toString() == "patterns") { + readDependencyPatternList(reader, info.m_DependencyPatterns); + } else { + reader.unexpected(); + } + } +} + +void FomodInstallerDialog::readPluginType(XmlReader& reader, Plugin& plugin) +{ + // Have a choice here of precisely one of 'type' or 'dependencytype', so this is + // not strictly necessary + plugin.m_PluginTypeInfo.m_DefaultType = TYPE_OPTIONAL; + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "type") { + plugin.m_PluginTypeInfo.m_DefaultType = + getPluginType(reader.attributes().value("name").toString()); + reader.finishedElement(); + } else if (reader.name().toString() == "dependencyType") { + readDependencyPluginType(reader, plugin.m_PluginTypeInfo); + } else { + reader.unexpected(); + } + } +} + +void FomodInstallerDialog::readConditionFlagList(XmlReader& reader, + ConditionFlagList& condflags) +{ + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "flag") { + QString name = reader.attributes().value("name").toString(); + QString content = reader.getText().trimmed(); + condflags.push_back(ConditionFlag(name, content)); + } else { + reader.unexpected(); + } + } +} + +bool FomodInstallerDialog::byPriority(const FileDescriptor* LHS, + const FileDescriptor* RHS) +{ + return LHS->m_Priority == RHS->m_Priority + ? LHS->m_FileSystemItemSequence < RHS->m_FileSystemItemSequence + : LHS->m_Priority < RHS->m_Priority; +} + +FomodInstallerDialog::Plugin FomodInstallerDialog::readPlugin(XmlReader& reader) +{ + Plugin result; + result.m_Name = reader.attributes().value("name").toString(); + + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "description") { + result.m_Description = reader.getText().trimmed(); + } else if (reader.name().toString() == "image") { + result.m_ImagePath = reader.attributes().value("path").toString(); + reader.finishedElement(); + } else if (reader.name().toString() == "files") { + readFileList(reader, result.m_Files); + } else if (reader.name().toString() == "conditionFlags") { + readConditionFlagList(reader, result.m_ConditionFlags); + } else if (reader.name().toString() == "typeDescriptor") { + readPluginType(reader, result); + } else { + reader.unexpected(); + } + } + + // I (TRT) am not quite sure why this sort is done here. It is done again + // when the files have been selected before installing them, which seems + // a more appropriate place. + std::sort(result.m_Files.begin(), result.m_Files.end(), byPriority); + + return result; +} + +FomodInstallerDialog::PluginType +FomodInstallerDialog::getPluginDependencyType(int page, + const PluginTypeInfo& info) const +{ + if (info.m_DependencyPatterns.size() != 0) { + for (const DependencyPattern& pattern : info.m_DependencyPatterns) { + if (testCondition(page, &pattern.condition).first) { + return pattern.type; + } + } + } + return info.m_DefaultType; +} + +void FomodInstallerDialog::readPluginList(XmlReader& reader, QString const& groupName, + GroupType& groupType, QLayout* layout) +{ + ItemOrder pluginOrder = + reader.attributes().hasAttribute("order") + ? getItemOrder(reader.attributes().value("order").toString()) + : ORDER_ASCENDING; + + // Read in all the plugins so we can check if the author is using "atmost" or + // "exactly", and correct as appropriate + std::vector plugins; + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "plugin") { + plugins.push_back(readPlugin(reader)); + } else { + reader.unexpected(); + } + } + + std::vector controls; + // This is somewhat of a hack. If the author has specified only 1 plugin and the + // group type is SELECTATLEASTONE or SELECTEXACTLYONE, then that plugin has to + // be selected. A note: This doesn't check for if somebody has defined a single + // plugin group with one of the above types, and then made the plugin unselectable. + // They deserve what they get. + // Similarly, if they've specfied SELECTATMOSTONE, we might as well give them + // a checkbox + if (plugins.size() == 1) { + switch (groupType) { + case TYPE_SELECTATLEASTONE: { + qWarning() << "Plugin " << plugins[0].m_Name + << " is the only plugin specified in group " << groupName + << " which requires selection of at least one plugin"; + groupType = TYPE_SELECTALL; + } break; + case TYPE_SELECTEXACTLYONE: { + qWarning() << "Plugin " << plugins[0].m_Name + << " is the only plugin specified in group " << groupName + << " which requires selection of exactly one plugin"; + groupType = TYPE_SELECTALL; + } break; + case TYPE_SELECTATMOSTONE: { + qWarning() << "Plugin " << plugins[0].m_Name + << " is the only plugin specified in group " << groupName + << " which permits selection of at most one plugin"; + groupType = TYPE_SELECTANY; + } break; + } + } + + for (Plugin const& plugin : plugins) { + QAbstractButton* newControl = nullptr; + switch (groupType) { + case TYPE_SELECTATLEASTONE: + case TYPE_SELECTANY: { + newControl = new QCheckBox(plugin.m_Name); + } break; + case TYPE_SELECTATMOSTONE: + case TYPE_SELECTEXACTLYONE: { + newControl = new QRadioButton(plugin.m_Name); + } break; + case TYPE_SELECTALL: { + newControl = new QCheckBox(plugin.m_Name); + newControl->setChecked(true); + newControl->setEnabled(false); + newControl->setToolTip(tr("All components in this group are required")); + } break; + } + newControl->setObjectName("choice"); + newControl->setAttribute(Qt::WA_Hover); + QVariant type(QVariant::fromValue(plugin.m_PluginTypeInfo)); + newControl->setProperty("plugintypeinfo", type); + newControl->setProperty("screenshot", plugin.m_ImagePath); + newControl->setProperty("description", plugin.m_Description); + QVariantList fileList; + // This looks horrible... + for (FileDescriptor* const& descriptor : plugin.m_Files) { + fileList.append(QVariant::fromValue(descriptor)); + } + newControl->setProperty("files", fileList); + QVariantList conditionFlags; + for (ConditionFlag const& conditionFlag : plugin.m_ConditionFlags) { + if (!conditionFlag.m_Name.isEmpty()) { + conditionFlags.append(QVariant::fromValue(conditionFlag)); + } + } + newControl->setProperty("conditionFlags", conditionFlags); + newControl->installEventFilter(this); + // We need somehow to check the 'toggled' signal. how do I do that + // void QAbstractButton::clicked ( bool checked ) [signal] + connect(newControl, SIGNAL(clicked()), this, SLOT(widgetButtonClicked())); + controls.push_back(newControl); + } + + if (pluginOrder == ORDER_ASCENDING) { + std::sort(controls.begin(), controls.end(), ControlsAscending); + } else if (pluginOrder == ORDER_DESCENDING) { + std::sort(controls.begin(), controls.end(), ControlsDescending); + } + + for (QAbstractButton* const control : controls) { + layout->addWidget(control); + } + + if (groupType == TYPE_SELECTATMOSTONE) { + QRadioButton* newButton = new QRadioButton(tr("None")); + newButton->setObjectName("none"); + layout->addWidget(newButton); + } +} + +void FomodInstallerDialog::readGroup(XmlReader& reader, QLayout* layout) +{ + QString name = reader.attributes().value("name").toString(); + GroupType type = getGroupType(reader.attributes().value("type").toString()); + + QGroupBox* groupBox = new QGroupBox(name); + + QVBoxLayout* groupLayout = new QVBoxLayout; + + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "plugins") { + readPluginList(reader, name, type, groupLayout); + } else { + reader.unexpected(); + } + } + + groupLayout->setProperty("groupType", QVariant::fromValue(type)); + groupLayout->setObjectName("grouplayout"); + groupBox->setLayout(groupLayout); + if (type == TYPE_SELECTATLEASTONE) { + QLabel* label = new QLabel(tr("Select one or more of these options:")); + layout->addWidget(label); + } + + layout->addWidget(groupBox); +} + +void FomodInstallerDialog::readGroupList(XmlReader& reader, QLayout* layout) +{ + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "group") { + readGroup(reader, layout); + } else { + reader.unexpected(); + } + } +} + +QGroupBox* FomodInstallerDialog::readInstallStep(XmlReader& reader) +{ + QString name = reader.attributes().value("name").toString(); + QGroupBox* page = new QGroupBox(name); + QVBoxLayout* pageLayout = new QVBoxLayout; + QScrollArea* scrollArea = new QScrollArea; + QFrame* scrolledArea = new QFrame; + QVBoxLayout* scrollLayout = new QVBoxLayout; + + SubCondition subcondition; + + // sequence: + // visible (optional) + // optionalFileGroups + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "visible") { + readCompositeDependency(reader, subcondition); + } else if (reader.name().toString() == "optionalFileGroups") { + readGroupList(reader, scrollLayout); + } else { + reader.unexpected(); + } + } + + if (subcondition.m_Conditions.size() != 0) { + // FIXME Is this actually OK? I'm storing a pointer in the property? + // Also AFAICS this is subject to memory leaks + page->setProperty("conditional", QVariant::fromValue(subcondition)); + } + + scrolledArea->setLayout(scrollLayout); + scrollArea->setWidget(scrolledArea); + scrollArea->setWidgetResizable(true); + pageLayout->addWidget(scrollArea); + page->setLayout(pageLayout); + return page; +} + +void FomodInstallerDialog::readStepList(XmlReader& reader) +{ + ItemOrder stepOrder = + reader.attributes().hasAttribute("order") + ? getItemOrder(reader.attributes().value("order").toString()) + : ORDER_ASCENDING; + + std::vector pages; + + // sequence installStep (1 or more) + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "installStep") { + pages.push_back(readInstallStep(reader)); + } else { + reader.unexpected(); + } + } + + if (stepOrder == ORDER_ASCENDING) { + std::sort(pages.begin(), pages.end(), PagesAscending); + } else if (stepOrder == ORDER_DESCENDING) { + std::sort(pages.begin(), pages.end(), PagesDescending); + } + + for (std::vector::const_iterator iter = pages.begin(); + iter != pages.end(); ++iter) { + ui->stepsStack->addWidget(*iter); + } +} + +void FomodInstallerDialog::readCompositeDependency(XmlReader& reader, + SubCondition& conditional) +{ + conditional.m_Operator = OP_AND; + if (reader.attributes().hasAttribute("operator")) { + auto opString = reader.attributes().value("operator").toString(); + if (opString == "Or") { + conditional.m_Operator = OP_OR; + } else if (opString != "And") { + qWarning() << "Expected 'and' or 'or' at line " << reader.lineNumber() << ", got " + << opString; + } // OP_AND is the default, set at the beginning of the function + } + + QString const self = reader.name().toString(); + while (reader.getNextElement(self)) { + auto elString = reader.name().toString(); + if (elString == "fileDependency") { + conditional.m_Conditions.push_back( + new FileCondition(reader.attributes().value("file").toString(), + reader.attributes().value("state").toString())); + reader.finishedElement(); + } else if (elString == "flagDependency") { + conditional.m_Conditions.push_back( + new ValueCondition(reader.attributes().value("flag").toString(), + reader.attributes().value("value").toString())); + reader.finishedElement(); + } else if (elString == "gameDependency") { + conditional.m_Conditions.push_back(new VersionCondition( + VersionCondition::v_Game, reader.attributes().value("version").toString())); + reader.finishedElement(); + } else if (elString == "fommDependency") { + conditional.m_Conditions.push_back(new VersionCondition( + VersionCondition::v_FOMM, reader.attributes().value("version").toString())); + reader.finishedElement(); + } else if (elString == "foseDependency") { + conditional.m_Conditions.push_back(new VersionCondition( + VersionCondition::v_FOSE, reader.attributes().value("version").toString())); + reader.finishedElement(); + } else if (elString == "dependencies") { + SubCondition* nested = new SubCondition(); + readCompositeDependency(reader, *nested); + conditional.m_Conditions.push_back(nested); + } else { + reader.unexpected(); + } + } + if (conditional.m_Conditions.size() == 0) { + qWarning() << "Empty conditional found at line " << reader.lineNumber(); + } +} + +FomodInstallerDialog::ConditionalInstall +FomodInstallerDialog::readConditionalInstallPattern(XmlReader& reader) +{ + ConditionalInstall result; + result.m_Condition.m_Operator = OP_AND; + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "dependencies") { + readCompositeDependency(reader, result.m_Condition); + } else if (reader.name().toString() == "files") { + readFileList(reader, result.m_Files); + } else { + reader.unexpected(); + } + } + return result; +} + +void FomodInstallerDialog::readConditionalFilePatternList(XmlReader& reader) +{ + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + if (reader.name().toString() == "pattern") { + m_ConditionalInstalls.push_back(readConditionalInstallPattern(reader)); + } else { + reader.unexpected(); + } + } +} + +void FomodInstallerDialog::readConditionalFileInstallList(XmlReader& reader) +{ + QString const self(reader.name().toString()); + // Technically there should be only one but it's easier to write like this + while (reader.getNextElement(self)) { + if (reader.name().toString() == "patterns") { + readConditionalFilePatternList(reader); + } else { + reader.unexpected(); + } + } +} + +void FomodInstallerDialog::readModuleConfiguration(XmlReader& reader) +{ + // sequence: + // modulename + // optional - moduleImage + // optional - moduleDependencies + // optional - requiredInstallFiles + // optional - installSteps + // optional - conditionalFileInstalls + QString const self(reader.name().toString()); + while (reader.getNextElement(self)) { + auto elString = reader.name().toString(); + if (elString == "moduleName") { + QString title = reader.getText(); + qDebug() << "module name : " << title; + } else if (elString == "moduleImage") { + // do something useful with the attributes of this + reader.finishedElement(); + } else if (elString == "moduleDependencies") { + SubCondition condition; + readCompositeDependency(reader, condition); + std::pair result = testCondition(-1, &condition); + if (!result.first) { + // TODO Better messages? + throw Exception(result.second); + } + } else if (elString == "requiredInstallFiles") { + readFileList(reader, m_RequiredFiles); + } else if (elString == "installSteps") { + readStepList(reader); + } else if (elString == "conditionalFileInstalls") { + readConditionalFileInstallList(reader); + } else { + reader.unexpected(); + } + } +} + +void FomodInstallerDialog::parseModuleConfig(XmlReader& reader) +{ + if (reader.readNext() != XmlReader::StartDocument) { + throw XmlParseError( + QString("Expected document start at line %1").arg(reader.lineNumber())); + } + processXmlTag(reader, "config", &FomodInstallerDialog::readModuleConfiguration); + if (reader.readNext() != XmlReader::EndDocument) { + throw XmlParseError( + QString("Expected document end at line %1").arg(reader.lineNumber())); + } + if (reader.hasError()) { + throw XmlParseError( + QString("%1 in line %2").arg(reader.errorString()).arg(reader.lineNumber())); + } + // Find the first visible page + int index = 0; + while (index < ui->stepsStack->count()) { + if (testVisible(index)) { + ui->stepsStack->setCurrentIndex(index); + displayCurrentPage(); + activateCurrentPage(); + break; + } + ++index; + } + // No pages are visible? Go to a small install + if (index >= ui->stepsStack->count()) { + transformToSmallInstall(); + } +} + +void FomodInstallerDialog::processXmlTag(XmlReader& reader, char const* tag, + TagProcessor func) +{ + if (reader.readNext() == XmlReader::StartElement && reader.name().toString() == tag) { + (this->*func)(reader); + } else if (!reader.hasError()) { + reader.raiseError( + QString("Expected %1, got %2").arg(tag).arg(reader.name().toString())); + } +} + +void FomodInstallerDialog::on_manualBtn_clicked() +{ + m_Manual = true; + this->reject(); +} + +void FomodInstallerDialog::on_cancelBtn_clicked() +{ + this->reject(); +} + +void FomodInstallerDialog::on_websiteLabel_linkActivated(const QString& link) +{ +#ifdef _WIN32 + ::ShellExecuteW(nullptr, L"open", ToWString(link).c_str(), nullptr, nullptr, + SW_SHOWNORMAL); +#else + QDesktopServices::openUrl(QUrl(link)); +#endif +} + +void FomodInstallerDialog::activateCurrentPage() +{ + QList choices = + ui->stepsStack->currentWidget()->findChildren("choice"); + if (choices.count() > 0) { + highlightControl(choices.at(0)); + } + m_PageVisible.push_back(true); + updateNextbtnText(); +} + +std::pair FomodInstallerDialog::testCondition(int maxIndex, + const QString& flag, + const QString& value) const +{ + // FIXME Review this and see if we can store the visible and evaluated variables for + // each page and cache like that. This would make me happier (if no one else) about + // the results of doing 'previous' and changing a flag to 'unset'. + + // iterate through all enabled condition flags on all activated controls on + // all visible pages if one of them matches the condition, taking the most + // recent setting. + for (int i = maxIndex - 1; i >= 0; --i) { + if (testVisible(i)) { + QWidget* page = ui->stepsStack->widget(i); + QList choices = page->findChildren("choice"); + for (QAbstractButton const* choice : choices) { + if (choice->isChecked()) { + QVariant temp = choice->property("conditionFlags"); + if (temp.isValid()) { + QVariantList conditionFlags = temp.toList(); + for (QVariant const& variant : conditionFlags) { + ConditionFlag condition = variant.value(); + if (condition.m_Name == flag) { + if (condition.m_Value == value) + return std::make_pair(true, tr("The flag '%1' matched '%2'") + .arg(condition.m_Name) + .arg(condition.m_Value)); + else + return std::make_pair(false, tr("The flag '%1' did not match '%2'") + .arg(condition.m_Name) + .arg(condition.m_Value)); + } + } + } + } + } + } + } + if (value.isEmpty()) + return std::make_pair(true, tr("The condition was not matched and is empty.")); + return std::make_pair(false, tr("The value exists but was not matched.")); +} + +bool FomodInstallerDialog::testVisible(int pageIndex) const +{ + if (pageIndex < static_cast(m_PageVisible.size())) { + return m_PageVisible[pageIndex]; + } + if (pageIndex >= ui->stepsStack->count()) { + return false; + } + QWidget* page = ui->stepsStack->widget(pageIndex); + QVariant subcond = page->property("conditional"); + if (subcond.isValid()) { + SubCondition subc = subcond.value(); + return testCondition(pageIndex, &subc).first; + } + return true; +} + +bool FomodInstallerDialog::nextPage() +{ + int oldIndex = ui->stepsStack->currentIndex(); + + int index = oldIndex + 1; + // find the next "visible" install step + while (index < ui->stepsStack->count()) { + if (testVisible(index)) { + ui->stepsStack->setCurrentIndex(index); + ui->stepsStack->currentWidget()->setProperty("previous", oldIndex); + return true; + } + m_PageVisible.push_back(false); + ++index; + } + // no more visible pages -> install + qWarning("Got to install after pressing next!"); + return false; +} + +void FomodInstallerDialog::widgetButtonClicked() +{ + // A button has been clicked. At the moment we do nothing with this + // beyond checking the next button state + updateNextbtnText(); +} + +void FomodInstallerDialog::updateNextbtnText() +{ + // First we see if we can actually allow the 'next' button. Specifically, this + // is a test to ensure that you have selected at least one item in a + //'select at least one' box. + int const page = ui->stepsStack->currentIndex(); + QStringList groups_requiring_selection; + for (QVBoxLayout const* const layout : + ui->stepsStack->widget(page)->findChildren("grouplayout")) { + GroupType const groupType(layout->property("groupType").value()); + if (groupType == TYPE_SELECTATLEASTONE) { + // Check at least one of this group is ticked + bool checked = false; + for (int i = 0; i != layout->count(); ++i) { + if (QLayoutItem* item = layout->itemAt(i)) { + QAbstractButton* const choice = + dynamic_cast(item->widget()); + if (choice != nullptr) { + if (choice->objectName() == "choice" && choice->isChecked()) { + checked = true; + break; + } + } + } + } + if (!checked) { + QString group = dynamic_cast(layout->parentWidget())->title(); + qDebug() << "Group " << group << " needs a selection"; + groups_requiring_selection.append(group); + } + } + } + + if (groups_requiring_selection.size() != 0) { + ui->nextBtn->setText(tr("Disabled")); + ui->nextBtn->setEnabled(false); + ui->nextBtn->setToolTip(tr("This button is disabled because the following group(s) " + "need a selection: ") + + groups_requiring_selection.join(", ")); + return; + } + + // OK, clear up any warnings + ui->nextBtn->setToolTip(""); + + // Display 'next' or 'install' as appropriate for the next button. + // note this can change depending on what buttons you click here. + + auto old_PageVisible = m_PageVisible; + ON_BLOCK_EXIT([&]() { + m_PageVisible = old_PageVisible; + }); + + bool isLast = true; + for (int index = page + 1; index != ui->stepsStack->count(); ++index) { + if (testVisible(index)) { + isLast = false; + break; + } + m_PageVisible.push_back(false); + } + + ui->nextBtn->setEnabled(true); + ui->nextBtn->setText(isLast ? tr("Install") : tr("Next")); +} + +void FomodInstallerDialog::displayCurrentPage() +{ + // Iterate over all buttons and set the tool tips as appropriate + int const page = ui->stepsStack->currentIndex(); + for (QVBoxLayout* layout : + ui->stepsStack->widget(page)->findChildren("grouplayout")) { + // Create a list of buttons, as in order to attempt to keep users existing choices + // intact, we may need to cycle over this twice + QList controls; + QAbstractButton* none_button(nullptr); + for (int i = 0; i != layout->count(); ++i) { + if (QLayoutItem* const item = layout->itemAt(i)) { + QAbstractButton* const choice = dynamic_cast(item->widget()); + if (choice != nullptr) { + if (choice->objectName() == "choice") { + controls.push_back(choice); + } else if (choice->objectName() == "none") { + none_button = choice; + } + } + } + } + + // FIXME If we are displaying this for the 2nd time, we should do two passes, + // as currently if you have decided against a recommended option, gone back, + // and then gone forward, your selection will be lost. + // For tick boxes it requires a bit of thought, because the first time we come + // in here, all tick boxes are clear, which is a valid condition. For radio + // buttons, that's not a valid condition so we can override. But we should + // possibly override anyway if the plugin types have changed since last time. + GroupType groupType(layout->property("groupType").value()); + if (groupType != TYPE_SELECTALL) { + bool const mustSelectOne = + groupType == TYPE_SELECTEXACTLYONE || groupType == TYPE_SELECTATLEASTONE; + bool maySelectMore = true; + QAbstractButton* first_optional = nullptr; + QAbstractButton* first_couldbe = nullptr; + + for (QAbstractButton* const control : controls) { + PluginTypeInfo const info = + control->property("plugintypeinfo").value(); + PluginType const type = getPluginDependencyType(page, info); + control->setEnabled(true); + switch (type) { + case TYPE_REQUIRED: { + if ((groupType == TYPE_SELECTEXACTLYONE) || + (groupType == TYPE_SELECTATMOSTONE)) { + // This only makes sense if the option may be disabled through + // conditions, so that if the conditions are met, this option is + // forced, otherwise the user can pick. + // This means that in this case the option is forced, and no user + // selection should be possible + for (QAbstractButton* groupControl : controls) { + groupControl->setEnabled(false); + } + } else { + control->setEnabled(false); + } + control->setChecked(true); + control->setToolTip(tr("This component is required")); + } break; + case TYPE_RECOMMENDED: { + if (maySelectMore || !mustSelectOne) { + control->setChecked(true); + } + control->setToolTip(tr("It is recommended you enable this component")); + } break; + case TYPE_OPTIONAL: { + if (first_optional == nullptr) { + first_optional = control; + } + control->setToolTip(tr("Optional component")); + } break; + case TYPE_NOTUSABLE: { + control->setChecked(false); + control->setEnabled(false); + control->setToolTip(tr("This component is not usable in combination with " + "other installed plugins")); + } break; + case TYPE_COULDBEUSABLE: { + if (first_couldbe == nullptr) { + first_couldbe = control; + } + control->setCheckable(true); + control->setIcon(QIcon(":/new/guiresources/warning_16")); + control->setToolTip(tr("You may be experiencing instability in combination " + "with other installed plugins")); + } break; + } + if (control->isChecked()) { + maySelectMore = false; + } + } + if (maySelectMore) { + if (none_button != nullptr) { + none_button->setChecked(true); + } else if (mustSelectOne) { + if (first_optional != nullptr) { + first_optional->setChecked(true); + } else if (first_couldbe != nullptr) { + qWarning("User should select at least one plugin but the only ones " + "available could cause instability"); + first_couldbe->setChecked(true); + } else { + // FIXME Should this generate an error + qWarning("User should select at least one plugin but none are available"); + controls[0]->setChecked(true); + } + } + } + } + } +} + +void FomodInstallerDialog::on_nextBtn_clicked() +{ + if (ui->stepsStack->currentIndex() == ui->stepsStack->count() - 1) { + this->accept(); + } else { + if (nextPage()) { + ui->prevBtn->setEnabled(true); + displayCurrentPage(); + activateCurrentPage(); + } else { + this->accept(); + } + } +} + +void FomodInstallerDialog::on_prevBtn_clicked() +{ + // FIXME this will go wrong if the first page isn't visible + if (ui->stepsStack->currentIndex() != 0) { + int previousIndex = 0; + QVariant temp = ui->stepsStack->currentWidget()->property("previous"); + if (temp.isValid()) { + previousIndex = temp.toInt(); + } else { + previousIndex = ui->stepsStack->currentIndex() - 1; + } + ui->stepsStack->setCurrentIndex(previousIndex); + m_PageVisible.resize(previousIndex); + ui->nextBtn->setText(tr("Next")); + } + if (ui->stepsStack->currentIndex() == 0) { + ui->prevBtn->setEnabled(false); + } + activateCurrentPage(); +} + +void FomodInstallerDialog::on_screenshotExpand_clicked() +{ + std::vector> carouselImages; + int carouselIndex = -1; + + for (auto choice : + ui->stepsStack->currentWidget()->findChildren("choice")) { + QString screenshotFileName = choice->property("screenshot").toString(); + + // If a choice has no screenshot, it should not be displayed in the screenshot + // dialog nor marked as the active carouselIndex + if (screenshotFileName.isEmpty()) { + continue; + } + + QString temp = QDir::tempPath() + "/" + m_FomodPath + "/" + + QDir::fromNativeSeparators(screenshotFileName); + carouselImages.push_back(std::pair(choice->text(), temp)); + + // Focus the screenshot carousel on the user's selected choice (or the first if + // there are multiple) + if (carouselIndex == -1 && choice->isChecked()) { + carouselIndex = ((int)carouselImages.size()) - 1; + } + } + + // Focus the screenshot carousel on the first screenshot if the user has not selected + // a choice with a screenshot (or any choice at all) + carouselIndex = (carouselIndex < 0) ? 0 : carouselIndex; + + QDialog* dialog = new FomodScreenshotDialog(this, carouselImages, carouselIndex); + dialog->show(); +} diff --git a/libs/installer_fomod/src/fomodinstallerdialog.h b/libs/installer_fomod/src/fomodinstallerdialog.h new file mode 100644 index 0000000..bd9f975 --- /dev/null +++ b/libs/installer_fomod/src/fomodinstallerdialog.h @@ -0,0 +1,476 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "installerfomod.h" + +class QAbstractButton; +class QXmlStreamReader; + +namespace Ui +{ +class FomodInstallerDialog; +} + +class ValueCondition; +class ConditionFlag; +class SubCondition; +class FileCondition; +class VersionCondition; + +class XmlReader; + +class IConditionTester +{ +public: + virtual std::pair + testCondition(int maxIndex, const ValueCondition* condition) const = 0; + virtual std::pair + testCondition(int maxIndex, const ConditionFlag* condition) const = 0; + virtual std::pair + testCondition(int maxIndex, const SubCondition* condition) const = 0; + virtual std::pair + testCondition(int maxIndex, const FileCondition* condition) const = 0; + virtual std::pair + testCondition(int maxIndex, const VersionCondition* condition) const = 0; +}; + +enum ConditionOperator +{ + OP_AND, + OP_OR +}; + +class Condition +{ +public: + Condition() {} + virtual std::pair test(int maxIndex, + const IConditionTester* tester) const = 0; + +private: + Condition& operator=(const Condition&) = delete; +}; + +class ConditionFlag : public Condition +{ +public: + ConditionFlag() : Condition(), m_Name(), m_Value() {} + ConditionFlag(const QString& name, const QString& value) + : Condition(), m_Name(name), m_Value(value) + {} + virtual std::pair test(int maxIndex, + const IConditionTester* tester) const + { + return tester->testCondition(maxIndex, this); + } + QString m_Name; + QString m_Value; +}; +Q_DECLARE_METATYPE(ConditionFlag) + +class ValueCondition : public Condition +{ +public: + ValueCondition() : Condition(), m_Name(), m_Value() {} + ValueCondition(const QString& name, const QString& value) + : Condition(), m_Name(name), m_Value(value) + {} + virtual std::pair test(int maxIndex, + const IConditionTester* tester) const + { + return tester->testCondition(maxIndex, this); + } + QString m_Name; + QString m_Value; +}; +Q_DECLARE_METATYPE(ValueCondition) + +class FileCondition : public Condition +{ +public: + FileCondition() : Condition(), m_File(), m_State() {} + FileCondition(const QString& file, const QString& state) + : Condition(), m_File(file), m_State(state) + {} + virtual std::pair test(int maxIndex, + const IConditionTester* tester) const + { + return tester->testCondition(maxIndex, this); + } + QString m_File; + QString m_State; +}; +Q_DECLARE_METATYPE(FileCondition) + +class SubCondition : public Condition +{ +public: + virtual std::pair test(int maxIndex, + const IConditionTester* tester) const + { + return tester->testCondition(maxIndex, this); + } + ConditionOperator m_Operator; + std::vector m_Conditions; +}; +Q_DECLARE_METATYPE(SubCondition) + +class VersionCondition : public Condition +{ +public: + enum Type + { + v_Game, + v_FOMM, + v_FOSE + }; + VersionCondition() : Condition(), m_Type(), m_RequiredVersion() {} + VersionCondition(Type type, const QString& requiredVersion) + : Condition(), m_Type(type), m_RequiredVersion(requiredVersion) + {} + virtual std::pair test(int maxIndex, + const IConditionTester* tester) const + { + return tester->testCondition(maxIndex, this); + } + Type m_Type; + QString m_RequiredVersion; +}; +Q_DECLARE_METATYPE(VersionCondition) + +class FileDescriptor : public QObject +{ + Q_OBJECT +public: + FileDescriptor(QObject* parent) + : QObject(parent), m_Source(), m_Destination(), m_Priority(0), m_IsFolder(false), + m_AlwaysInstall(false), m_InstallIfUsable(false), m_FileSystemItemSequence(0) + {} + + FileDescriptor(const FileDescriptor& reference) + : QObject(reference.parent()), m_Source(reference.m_Source), + m_Destination(reference.m_Destination), m_Priority(reference.m_Priority), + m_IsFolder(reference.m_IsFolder), m_AlwaysInstall(reference.m_AlwaysInstall), + m_InstallIfUsable(reference.m_InstallIfUsable), + m_FileSystemItemSequence(reference.m_FileSystemItemSequence) + {} + + QString m_Source; + QString m_Destination; + int m_Priority; + bool m_IsFolder; + bool m_AlwaysInstall; + bool m_InstallIfUsable; + int m_FileSystemItemSequence; + +private: + FileDescriptor& operator=(const FileDescriptor&); +}; + +Q_DECLARE_METATYPE(FileDescriptor*) + +class FomodInstallerDialog : public QDialog, public IConditionTester +{ + Q_OBJECT + +public: + explicit FomodInstallerDialog( + InstallerFomod* installer, const MOBase::GuessedValue& modName, + const QString& fomodPath, + const std::function& fileCheck, + QWidget* parent = 0); + ~FomodInstallerDialog(); + + void initData(MOBase::IOrganizer* moInfo); + + /** + * @return bool true if the user requested the manual dialog + **/ + bool manualRequested() const { return m_Manual; } + + /** + * @return the (user-modified) name to be used for the mod + **/ + QString getName() const; + + /** + * @return the version of the mod as specified in the fomod info.xml + */ + QString getVersion() const; + + /** + * @return the mod id as specified in the info.xml + */ + int getModID() const; + + /** + * @return the mod url as specified in the fomod file + */ + QString getURL() const; + + /** + * @brief Updated the archive tree from the dialog. + * + * @param tree The input archive tree. + **/ + MOBase::IPluginInstaller::EInstallResult + updateTree(std::shared_ptr& tree); + + bool hasOptions(); + + void transformToSmallInstall(); + +protected: + virtual bool eventFilter(QObject* object, QEvent* event); + +private slots: + + void on_cancelBtn_clicked(); + + void on_manualBtn_clicked(); + + void on_websiteLabel_linkActivated(const QString& link); + + void on_nextBtn_clicked(); + + void on_prevBtn_clicked(); + + // detect signals for people playing with checkboxes/buttons + void widgetButtonClicked(); + + void on_screenshotExpand_clicked(); + +private: + enum ItemOrder + { + ORDER_ASCENDING, + ORDER_DESCENDING, + ORDER_EXPLICIT + }; + + // So I can make GroupType and PluginTypeInfo into QVariants +public: + enum GroupType + { + TYPE_SELECTATLEASTONE, + TYPE_SELECTATMOSTONE, + TYPE_SELECTEXACTLYONE, + TYPE_SELECTANY, + TYPE_SELECTALL + }; + + enum PluginType + { + TYPE_REQUIRED, + TYPE_RECOMMENDED, + TYPE_OPTIONAL, + TYPE_NOTUSABLE, + TYPE_COULDBEUSABLE + }; + + struct DependencyPattern + { + PluginType type; + SubCondition condition; + }; + + typedef std::vector DependencyPatternList; + + struct PluginTypeInfo + { + PluginType m_DefaultType; + DependencyPatternList m_DependencyPatterns; + }; + +private: + typedef std::vector FileDescriptorList; + typedef std::vector ConditionFlagList; + + struct Plugin + { + QString m_Name; + QString m_Description; + QString m_ImagePath; + PluginTypeInfo m_PluginTypeInfo; + ConditionFlagList m_ConditionFlags; + FileDescriptorList m_Files; + }; + + struct ConditionalInstall + { + SubCondition m_Condition; + FileDescriptorList m_Files; + }; + + struct LeafInfo + { + int priority; + QString path; + }; + + using Leaves = std::map; + +private: + QString readContent(QXmlStreamReader& reader); + + /** + * @brief Read XML from the given file, trying various encoding, and using + * the given callback on each try. + * + * @param file The file to read, must already be opened. + * @param callback The callback used for every encoding try. + */ + void readXml(QFile& file, void (FomodInstallerDialog::*callback)(XmlReader&)); + + void readInfoXml(); + void readModuleConfigXml(); + + void parseInfo(XmlReader& data); + void parseModuleConfig(XmlReader& data); + + void updateNameEdit(); + + static int bomOffset(const QByteArray& buffer); + static ItemOrder getItemOrder(const QString& orderString); + static GroupType getGroupType(const QString& typeString); + static PluginType getPluginType(const QString& typeString); + static bool byPriority(const FileDescriptor* LHS, const FileDescriptor* RHS); + + PluginType getPluginDependencyType(int page, PluginTypeInfo const& info) const; + + typedef void (FomodInstallerDialog::*TagProcessor)(XmlReader& reader); + void processXmlTag(XmlReader& reader, char const* tag, TagProcessor func); + + void readFileList(XmlReader& reader, FileDescriptorList& fileList); + void readDependencyPattern(XmlReader& reader, DependencyPattern& pattern); + void readDependencyPatternList(XmlReader& reader, DependencyPatternList& patterns); + void readDependencyPluginType(XmlReader& reader, PluginTypeInfo& info); + void readPluginType(XmlReader& reader, Plugin& plugin); + void readConditionFlagList(XmlReader& reader, ConditionFlagList& condflags); + FomodInstallerDialog::Plugin readPlugin(XmlReader& reader); + void readPluginList(XmlReader& reader, QString const& groupName, GroupType& groupType, + QLayout* layout); + void readGroup(XmlReader& reader, QLayout* layout); + void readGroupList(XmlReader& reader, QLayout* layout); + QGroupBox* readInstallStep(XmlReader& reader); + void readCompositeDependency(XmlReader& reader, SubCondition& conditional); + ConditionalInstall readConditionalInstallPattern(XmlReader& reader); + void readConditionalFilePatternList(XmlReader& reader); + void readConditionalFileInstallList(XmlReader& reader); + void readStepList(XmlReader& reader); + void readModuleConfiguration(XmlReader& reader); + void highlightControl(QAbstractButton* button); + + std::pair testCondition(int maxIndex, const QString& flag, + const QString& value) const; + virtual std::pair testCondition(int maxIndex, + const ValueCondition* condition) const; + virtual std::pair testCondition(int maxIndex, + const ConditionFlag* condition) const; + virtual std::pair testCondition(int maxIndex, + const SubCondition* condition) const; + virtual std::pair testCondition(int maxIndex, + const FileCondition* condition) const; + virtual std::pair + testCondition(int maxIndex, const VersionCondition* condition) const; + bool testVisible(int pageIndex) const; + bool nextPage(); + void activateCurrentPage(); + + void moveTree(std::shared_ptr target, + std::shared_ptr source, + MOBase::IFileTree::OverwritesType& overwrites); + + void copyLeaf(std::shared_ptr sourceEntry, + std::shared_ptr destinationTree, + QString destinationPath, MOBase::IFileTree::OverwritesType& overwrites, + Leaves& leaves, int pri); + + bool copyFileIterator(std::shared_ptr sourceTree, + std::shared_ptr destinationTree, + const FileDescriptor* descriptor, Leaves& leaves, + MOBase::IFileTree::OverwritesType& overwrites); + + static void applyPriority(Leaves& leaves, MOBase::IFileTree const* tree, + int priority); + + /** + * @brief Display a dialog indicating to the user that some files were not found. + * + * @param missingFiles List of missing files. + * + * @return true if the user chose to continue with the installation, false otherwize. + */ + bool displayMissingFilesDialog(std::vector missingFiles); + + static QString toString(MOBase::IPluginList::PluginStates state); + + // Set the 'next' button to display 'next' or 'install' + void updateNextbtnText(); + + // Display the current page calculating all the button enables/disables + void displayCurrentPage(); + +private: + Ui::FomodInstallerDialog* ui; + + InstallerFomod* m_Installer; + MOBase::GuessedValue m_ModName; + + int m_ModID; + + QString m_FomodPath; + bool m_Manual; + + FileDescriptorList m_RequiredFiles; + std::vector m_ConditionalInstalls; + std::vector m_PageVisible; + + std::function m_FileCheck; + + // Because NMM maintains the sequence from the xml when dealing with things with + // the same priority, we have to as well. This is moderately hacky. + int m_FileSystemItemSequence; + + // So I can find out game info (I hope) + MOBase::IOrganizer* m_MoInfo; + + // The web page in the fomod (if supplied) + QString m_URL; +}; + +Q_DECLARE_METATYPE(FomodInstallerDialog::GroupType) +Q_DECLARE_METATYPE(FomodInstallerDialog::PluginTypeInfo) diff --git a/libs/installer_fomod/src/fomodinstallerdialog.ui b/libs/installer_fomod/src/fomodinstallerdialog.ui new file mode 100644 index 0000000..9003416 --- /dev/null +++ b/libs/installer_fomod/src/fomodinstallerdialog.ui @@ -0,0 +1,269 @@ + + + FomodInstallerDialog + + + + 0 + 0 + 769 + 477 + + + + FOMOD Installer + + + + + + 0 + + + + + Name + + + + + + + + 0 + 0 + + + + true + + + + + + + + + QFormLayout::AllNonFixedFieldsGrow + + + 15 + + + + + Author + + + + + + + Version + + + + + + + + + + + + + + Website + + + + + + + <a href="#">Link</a> + + + + + + + + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + false + + + + Qt::Vertical + + + false + + + + + 0 + 0 + + + + + 0 + 100 + + + + true + + + + + + 0 + 0 + + + + + 320 + 200 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + PointingHandCursor + + + border:none; background:none + + + + + + + + + + + + + 200 + 0 + + + + -1 + + + + + + + + + + Manual + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Back + + + false + + + + + + + Next + + + + + + + Cancel + + + false + + + false + + + + + + + + + + ScaleLabel + QLabel +
scalelabel.h
+
+
+ + +
diff --git a/libs/installer_fomod/src/fomodscreenshotdialog.cpp b/libs/installer_fomod/src/fomodscreenshotdialog.cpp new file mode 100644 index 0000000..568faa4 --- /dev/null +++ b/libs/installer_fomod/src/fomodscreenshotdialog.cpp @@ -0,0 +1,170 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ +#include "fomodscreenshotdialog.h" +#include "ui_fomodscreenshotdialog.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "scalelabel.h" + +constexpr int kScreenshotTileWidth = 100; +constexpr int kScreenshotTileHeight = 80; +constexpr int kScreenshotTileSpacing = 16; + +// Disables the native dotted selection border around focused elements. Unfortunately, +// disabling it via stylesheets is broken in the latest version of Qt +// https://stackoverflow.com/questions/9795791/removing-dotted-border-without-setting-nofocus-in-windows-pyqt +class NoFocusProxyStyle : public QProxyStyle +{ +public: + NoFocusProxyStyle(QStyle* baseStyle = nullptr) : QProxyStyle(baseStyle) {} + + void drawPrimitive(PrimitiveElement element, const QStyleOption* option, + QPainter* painter, const QWidget* widget) const + { + if (element == QStyle::PE_FrameFocusRect) { + return; + } + QProxyStyle::drawPrimitive(element, option, painter, widget); + } +}; + +FomodScreenshotDialog::FomodScreenshotDialog( + QWidget* parent, std::vector> carouselImages, + int carouselIndex) + : QDialog(parent, Qt::FramelessWindowHint), ui(new Ui::FomodScreenshotDialog), + m_carouselImages(carouselImages) +{ + Q_INIT_RESOURCE(resources); + + ui->setupUi(this); + setAttribute(Qt::WA_TranslucentBackground); + + // Manually maximize the dialog since showMaximized() clips over the taskbar + QScreen* screen = this->screen(); + QRect availableGeometry = screen->availableGeometry(); + setFixedSize(availableGeometry.width(), availableGeometry.height()); + move(availableGeometry.x(), availableGeometry.y()); + + QTableWidget* carouselList = ui->carouselList; + carouselList->setStyle(new NoFocusProxyStyle); + + carouselList->setRowCount(1); + carouselList->setRowHeight(0, kScreenshotTileHeight); + carouselList->setColumnCount(0); + for (auto carouselImage : m_carouselImages) { + QFrame* container = new QFrame(carouselList); + QVBoxLayout* layout = new QVBoxLayout(container); + layout->setContentsMargins(0, 0, 0, 0); + container->setLayout(layout); + + ScaleLabel* scaleLabel = new ScaleLabel(container); + scaleLabel->setScalableResource(carouselImage.second); + scaleLabel->setFixedSize(kScreenshotTileWidth, kScreenshotTileHeight); + scaleLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + scaleLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + scaleLabel->setCursor(Qt::PointingHandCursor); + scaleLabel->setStatic(true); + + layout->addWidget(scaleLabel); + layout->setAlignment(scaleLabel, Qt::AlignLeft | Qt::AlignVCenter); + + int column = carouselList->columnCount(); + carouselList->setColumnCount(column + 1); + carouselList->setColumnWidth( + column, kScreenshotTileWidth + + (column + 1 == carouselImages.size() ? 0 : kScreenshotTileSpacing)); + + QTableWidgetItem* item = new QTableWidgetItem(""); + carouselList->setItem(0, column, item); + carouselList->setCellWidget(0, column, container); + } + + connect(carouselList, &QTableWidget::itemSelectionChanged, this, + &FomodScreenshotDialog::selectedScreenshotChanged); + ui->carouselList->selectColumn(carouselIndex); +} + +FomodScreenshotDialog::~FomodScreenshotDialog() +{ + delete ui; +} + +void FomodScreenshotDialog::on_closeButton_clicked() +{ + close(); +} + +void FomodScreenshotDialog::on_navigateLeft_clicked() +{ + int selectedColumn = getSelectedScreenshot(); + if (selectedColumn == 0) { + return; + } + + ui->carouselList->selectColumn(selectedColumn - 1); +} + +void FomodScreenshotDialog::on_navigateRight_clicked() +{ + int selectedColumn = getSelectedScreenshot(); + if (selectedColumn == m_carouselImages.size() - 1) { + return; + } + + ui->carouselList->selectColumn(selectedColumn + 1); +} + +void FomodScreenshotDialog::selectedScreenshotChanged() +{ + // In case the user ctrl+clicks current selection to result in an empty selection + if (ui->carouselList->selectedItems().isEmpty()) { + ui->carouselList->selectColumn(0); + return; + } + + int selectedColumn = getSelectedScreenshot(); + + ui->imageTitleLabel->setText(m_carouselImages.at(selectedColumn).first); + ui->image->setScalableResource(m_carouselImages.at(selectedColumn).second); + ui->slideshowPosition->setText(QString("%1/%2").arg( + QString::number(selectedColumn + 1), QString::number(m_carouselImages.size()))); + + for (int column = 0; column < ui->carouselList->columnCount(); column++) { + QWidget* widget = ui->carouselList->cellWidget(0, column); + ScaleLabel* scaleLabel = widget->findChild(); + if (column == selectedColumn) { + scaleLabel->setStyleSheet(scaleLabel->styleSheet() + + "QLabel { border:2px solid white; }"); + } else { + scaleLabel->setStyleSheet(scaleLabel->styleSheet() + "QLabel { border:none; }"); + } + } +} + +int FomodScreenshotDialog::getSelectedScreenshot() +{ + return ui->carouselList->selectedItems().front()->column(); +} diff --git a/libs/installer_fomod/src/fomodscreenshotdialog.h b/libs/installer_fomod/src/fomodscreenshotdialog.h new file mode 100644 index 0000000..dfe2cfd --- /dev/null +++ b/libs/installer_fomod/src/fomodscreenshotdialog.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include +#include + +namespace Ui +{ +class FomodScreenshotDialog; +} + +class FomodScreenshotDialog : public QDialog +{ + Q_OBJECT + +public: + explicit FomodScreenshotDialog( + QWidget* parent, std::vector> carouselImages, + int carouselIndex); + ~FomodScreenshotDialog(); + +private slots: + void on_closeButton_clicked(); + void on_navigateLeft_clicked(); + void on_navigateRight_clicked(); + +private: + void selectedScreenshotChanged(); + int getSelectedScreenshot(); + + Ui::FomodScreenshotDialog* ui; + std::vector> m_carouselImages; +}; diff --git a/libs/installer_fomod/src/fomodscreenshotdialog.ui b/libs/installer_fomod/src/fomodscreenshotdialog.ui new file mode 100644 index 0000000..ea1ac9a --- /dev/null +++ b/libs/installer_fomod/src/fomodscreenshotdialog.ui @@ -0,0 +1,475 @@ + + + FomodScreenshotDialog + + + + 0 + 0 + 1020 + 693 + + + + background:none; + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame#backgroundFrame{background-color: rgba(0,0,0,85%);} + + + + + + 0 + + + + + + 10 + + + + QLabel { color:white; } + + + 001/999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 32 + 32 + + + + + 32 + 32 + + + + PointingHandCursor + + + Qt::NoFocus + + + border:none; + + + + + + + :/MO/gui/resources/CloseButtonIcon.png:/MO/gui/resources/CloseButtonIcon.png + + + false + + + false + + + + + + + + + + + + 0 + 0 + + + + + 75 + 0 + + + + PointingHandCursor + + + Qt::NoFocus + + + QPushButton{border:none; } QPushButton:hover{background-color:rgba(255,255,255,25%); } + + + + + + + :/MO/gui/resources/LeftButtonIcon.png:/MO/gui/resources/LeftButtonIcon.png + + + + 32 + 32 + + + + false + + + + + + + + 0 + 0 + + + + + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 75 + 0 + + + + PointingHandCursor + + + Qt::NoFocus + + + QPushButton{border:none; } QPushButton:hover{background-color:rgba(255,255,255,25%); } + + + + + + + :/MO/gui/resources/RightButtonIcon.png:/MO/gui/resources/RightButtonIcon.png + + + + 32 + 32 + + + + false + + + false + + + + + + + + + + 0 + 0 + + + + + 12 + + + + QLabel { color:white; } + + + <image name> + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 16777215 + 100 + + + + QTableWidget { + background-color:transparent; + outline:none; +} + +QTableWidget::item { + selection-background-color:transparent; + background: none; +} + +QScrollBar:horizontal { + border: none; + background-color:transparent; + height: 15px; + margin: 0px 0px 0px 0px; +} +QScrollBar::handle:horizontal { + border-radius: 2px; + background-color: rgba(255,255,255,85%); + min-width: 20px; +} + +QScrollBar::add-line:horizontal { + width: 0px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal { + width: 0px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { + background: none; +} + + + QFrame::NoFrame + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectColumns + + + + 100 + 80 + + + + false + + + 1 + + + false + + + false + + + + + New Column + + + + + New Column + + + + + New Column + + + + + New Column + + + + + New Column + + + + + New Column + + + + + New Column + + + + + New Column + + + + + New Column + + + + + New Column + + + + + New Column + + + + + New Column + + + + + New Column + + + + + sdf + + + + + ssdf + + + + + sf + + + + + sdf + + + + + sdf + + + + + dsfdf + + + + + sdfsfd + + + + + sdfsdf + + + + + sdfsdf + + + + + sdf + + + + + sdf + + + + + sdf + + + + + sdf + + + + + + + + + + + + ScaleLabel + QLabel +
scalelabel.h
+
+
+ + + + +
diff --git a/libs/installer_fomod/src/installer_fomod_en.ts b/libs/installer_fomod/src/installer_fomod_en.ts new file mode 100644 index 0000000..98c5d97 --- /dev/null +++ b/libs/installer_fomod/src/installer_fomod_en.ts @@ -0,0 +1,274 @@ + + + + + FomodInstallerDialog + + + FOMOD Installer + + + + + Name + + + + + Author + + + + + Version + + + + + Website + + + + + <a href="#">Link</a> + + + + + Manual + + + + + Back + + + + + + + Next + + + + + + Cancel + + + + + Failed to parse %1. See console for details. + + + + + %1 missing. + + + + + At least one condition was successful in an 'OR' clause! + + + + + All conditions were successful in an 'AND' clause! + + + + + No conditions were successful in an 'OR' clause! + + + + + invalid plugin state %1 + + + + + Missing + + + + + Inactive + + + + + Active + + + + + Success: The file '%1' was marked %2. + + + + + Missing requirement: The file '%1' should be %2, but was %3! + + + + + Success: The required version of %1 is %2, and was detected as %3. + + + + + Missing requirement: The required version of %1 is %2, but was detected as %3. + + + + + Missing files or folders + + + + + Install anyway + + + + + The following files or folders were not found in the archive. This is likely due to an incorrect FOMOD installer. This mod may not work properly. + + + + + Folder '%1'. + + + + + File '%1'. + + + + + <a href="%1">Link</a> + + + + + unsupported order type %1 + + + + + unsupported group type %1 + + + + + All components in this group are required + + + + + None + + + + + Select one or more of these options: + + + + + The flag '%1' matched '%2' + + + + + The flag '%1' did not match '%2' + + + + + The condition was not matched and is empty. + + + + + The value exists but was not matched. + + + + + Disabled + + + + + This button is disabled because the following group(s) need a selection: + + + + + Install + + + + + This component is required + + + + + It is recommended you enable this component + + + + + Optional component + + + + + This component is not usable in combination with other installed plugins + + + + + You may be experiencing instability in combination with other installed plugins + + + + + InstallerFomod + + + Installer for xml based fomod archives. + + + + + Fomod Installer + + + + + Installation as fomod failed: %1 + + + + + image formats not supported. + + + + + + invalid problem key %1 + + + + + This indicates that files from dlls/imageformats are missing from your MO installation or outdated. Images in installers may not be displayed. Please re-install MO + + + + diff --git a/libs/installer_fomod/src/installerfomod.cpp b/libs/installer_fomod/src/installerfomod.cpp new file mode 100644 index 0000000..3558748 --- /dev/null +++ b/libs/installer_fomod/src/installerfomod.cpp @@ -0,0 +1,301 @@ +#include "installerfomod.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "fomodinstallerdialog.h" + +using namespace MOBase; + +const unsigned int InstallerFomod::PROBLEM_IMAGETYPE_UNSUPPORTED; + +InstallerFomod::InstallerFomod() : m_MOInfo(nullptr) {} + +bool InstallerFomod::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + return true; +} + +QString InstallerFomod::name() const +{ + return "Fomod Installer"; +} + +QString InstallerFomod::author() const +{ + return "Tannin & thosrtanner"; +} + +QString InstallerFomod::description() const +{ + return tr("Installer for xml based fomod archives."); +} + +VersionInfo InstallerFomod::version() const +{ + return VersionInfo(1, 7, 0, VersionInfo::RELEASE_FINAL); +} + +QString InstallerFomod::localizedName() const +{ + return tr("Fomod Installer"); +} + +bool InstallerFomod::allowAnyFile() const +{ + return m_MOInfo->pluginSetting(name(), "use_any_file").toBool(); +} + +bool InstallerFomod::checkDisabledMods() const +{ + return m_MOInfo->pluginSetting(name(), "see_disabled_mods").toBool(); +} + +QList InstallerFomod::settings() const +{ + QList result; + result.push_back( + PluginSetting("prefer", "prefer this over the NCC based plugin", QVariant(true))); + result.push_back(PluginSetting("use_any_file", + "allow dependencies on any file, not just esp/esm", + QVariant(false))); + result.push_back(PluginSetting("see_disabled_mods", + "treat disabled mods as inactive rather than missing", + QVariant(false))); + return result; +} + +unsigned int InstallerFomod::priority() const +{ + return m_MOInfo->pluginSetting(name(), "prefer").toBool() ? 110 : 90; +} + +bool InstallerFomod::isManualInstaller() const +{ + return false; +} + +void InstallerFomod::onInstallationStart(QString const&, bool, IModInterface*) +{ + m_InstallerUsed = false; +} + +void InstallerFomod::onInstallationEnd(EInstallResult result, IModInterface* newMod) +{ + if (result == EInstallResult::RESULT_SUCCESS && m_InstallerUsed && + newMod->url().isEmpty()) { + newMod->setUrl(m_Url); + } +} + +std::shared_ptr +InstallerFomod::findFomodDirectory(std::shared_ptr tree) const +{ + auto entry = tree->find("fomod", FileTreeEntry::DIRECTORY); + + if (entry != nullptr) { + return entry->astree(); + } + + if (tree->size() == 1 && tree->at(0)->isDir()) { + return findFomodDirectory(tree->at(0)->astree()); + } + return nullptr; +} + +bool InstallerFomod::isArchiveSupported(std::shared_ptr tree) const +{ + tree = findFomodDirectory(tree); + if (tree != nullptr) { + return tree->exists("ModuleConfig.xml", FileTreeEntry::FILE); + } + return false; +} + +void InstallerFomod::appendImageFiles( + std::vector>& entries, + std::shared_ptr tree) const +{ + static std::set imageSuffixes{"png", "jpg", "jpeg", + "gif", "bmp"}; + for (auto entry : *tree) { + if (entry->isDir()) { + appendImageFiles(entries, entry->astree()); + } else if (imageSuffixes.count(entry->suffix()) > 0) { + entries.push_back(entry); + } + } +} + +std::vector> +InstallerFomod::buildFomodTree(std::shared_ptr tree) const +{ + std::vector> entries; + + auto fomodTree = findFomodDirectory(tree); + + for (auto entry : *fomodTree) { + if (entry->isFile() && + (entry->compare("info.xml") == 0 || entry->compare("ModuleConfig.xml") == 0)) { + entries.push_back(entry); + } + } + + appendImageFiles(entries, tree); + + return entries; +} + +IPluginList::PluginStates InstallerFomod::fileState(const QString& fileName) const +{ + QString ext = QFileInfo(fileName).suffix().toLower(); + if ((ext == "esp") || (ext == "esm") || (ext == "esl")) { + IPluginList::PluginStates state = m_MOInfo->pluginList()->state(fileName); + if (state != IPluginList::STATE_MISSING) { + return state; + } + } else if (allowAnyFile()) { + QFileInfo info(fileName); + QString name = info.fileName(); + QStringList files = + m_MOInfo->findFiles(info.dir().path(), [&, name](const QString& f) -> bool { + return name.compare(QFileInfo(f).fileName(), + FileNameComparator::CaseSensitivity) == 0; + }); + // A note: The list of files produced is somewhat odd as it's the full path + // to the originating mod (or mods). However, all we care about is if it's + // there or not. + if (files.size() != 0) { + return IPluginList::STATE_ACTIVE; + } + } else { + log::warn("A dependency on non esp/esm/esl {} will always find it as missing.", + fileName); + return IPluginList::STATE_MISSING; + } + + // If they are really desparate we look in the full mod list and try that + if (checkDisabledMods()) { + IModList* modList = m_MOInfo->modList(); + QStringList list = modList->allMods(); + for (QString mod : list) { + // Get mod state. if it's active we've already looked. If it's not valid, + // no point in looking. + IModList::ModStates state = modList->state(mod); + if ((state & IModList::STATE_ACTIVE) != 0 || + (state & IModList::STATE_VALID) == 0) { + continue; + } + MOBase::IModInterface* modInfo = m_MOInfo->modList()->getMod(mod); + // Go see if the file is in the mod + QDir modpath(modInfo->absolutePath()); + QFile file(modpath.absoluteFilePath(fileName)); + if (file.exists()) { + return IPluginList::STATE_INACTIVE; + } + } + } + return IPluginList::STATE_MISSING; +} + +IPluginInstaller::EInstallResult +InstallerFomod::install(GuessedValue& modName, + std::shared_ptr& tree, QString& version, int& modID) +{ + auto installerFiles = buildFomodTree(tree); + if (manager()->extractFiles(installerFiles).size() == installerFiles.size()) { + try { + std::shared_ptr fomodTree = findFomodDirectory(tree); + + QString fomodPath = fomodTree->parent()->path(); + FomodInstallerDialog dialog( + this, modName, fomodPath, + std::bind(&InstallerFomod::fileState, this, std::placeholders::_1)); + dialog.initData(m_MOInfo); + if (!dialog.getVersion().isEmpty()) { + version = dialog.getVersion(); + } + if (dialog.getModID() != -1) { + modID = dialog.getModID(); + } + + m_InstallerUsed = true; + m_Url = dialog.getURL(); + + if (!dialog.hasOptions()) { + dialog.transformToSmallInstall(); + } + + auto result = dialog.exec(); + if (result == QDialog::Accepted) { + modName.update(dialog.getName(), GUESS_USER); + return dialog.updateTree(tree); + } else { + if (dialog.manualRequested()) { + modName.update(dialog.getName(), GUESS_USER); + return IPluginInstaller::RESULT_MANUALREQUESTED; + } else if (result == QDialog::Rejected) { + return IPluginInstaller::RESULT_CANCELED; + } else { + return IPluginInstaller::RESULT_FAILED; + } + } + } catch (const std::exception& e) { + reportError(tr("Installation as fomod failed: %1").arg(e.what())); + return IPluginInstaller::RESULT_FAILED; + } + } + return IPluginInstaller::RESULT_CANCELED; +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(installerFomod, InstallerFomod) +#endif + +std::vector InstallerFomod::activeProblems() const +{ + std::vector result; + QList formats = QImageReader::supportedImageFormats(); + if (!formats.contains("jpg")) { + result.push_back(PROBLEM_IMAGETYPE_UNSUPPORTED); + } + return result; +} + +QString InstallerFomod::shortDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_IMAGETYPE_UNSUPPORTED: + return tr("image formats not supported."); + default: + throw Exception(tr("invalid problem key %1").arg(key)); + } +} + +QString InstallerFomod::fullDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_IMAGETYPE_UNSUPPORTED: + return tr("This indicates that files from dlls/imageformats are missing from your " + "MO installation or outdated. " + "Images in installers may not be displayed. Please re-install MO"); + default: + throw Exception(tr("invalid problem key %1").arg(key)); + } +} + +bool InstallerFomod::hasGuidedFix(unsigned int) const +{ + return false; +} + +void InstallerFomod::startGuidedFix(unsigned int) const {} diff --git a/libs/installer_fomod/src/installerfomod.h b/libs/installer_fomod/src/installerfomod.h new file mode 100644 index 0000000..1ba8ed8 --- /dev/null +++ b/libs/installer_fomod/src/installerfomod.h @@ -0,0 +1,100 @@ +#ifndef INSTALLERFOMOD_H +#define INSTALLERFOMOD_H + +#include +#include +#include + +class InstallerFomod : public MOBase::IPluginInstallerSimple, + public MOBase::IPluginDiagnose +{ + + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple + MOBase::IPluginDiagnose) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.InstallerFomod") +#endif + +public: + InstallerFomod(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + + virtual unsigned int priority() const override; + virtual bool isManualInstaller() const override; + + virtual bool + isArchiveSupported(std::shared_ptr tree) const override; + virtual EInstallResult install(MOBase::GuessedValue& modName, + std::shared_ptr& tree, + QString& version, int& modID) override; + + virtual void onInstallationStart(QString const& archive, bool reinstallation, + MOBase::IModInterface* currentMod) override; + virtual void onInstallationEnd(EInstallResult result, + MOBase::IModInterface* newMod) override; + +public: // IPluginDiagnose interface + virtual std::vector activeProblems() const; + virtual QString shortDescription(unsigned int key) const; + virtual QString fullDescription(unsigned int key) const; + virtual bool hasGuidedFix(unsigned int key) const; + virtual void startGuidedFix(unsigned int key) const; + +private: + /** + * @brief Retrieve the tree entry corresponding to the fomod directory. + * + * @param tree Tree to look-up the directory in. + * + * @return the entry corresponding to the fomod directory in the tree, or a null + * pointer if the entry was not found. + */ + std::shared_ptr + findFomodDirectory(std::shared_ptr tree) const; + + /** + * @brief Build a list of entries that should be extracted sincce the FOMOD installer + * may require access to (currently the .xml files in the FOMOD directory and the + * pictures in the archive). + * + * @param tree Base tree of the archive. + * + * @return a list of file entries that need to be extracted. + */ + std::vector> + buildFomodTree(std::shared_ptr tree) const; + + /** + * @brief Recurse through the given tree and add all the images to the given vector. + * + * @param result Vector of entries to add the images. + * @param tree The tree to look files in. + */ + void + appendImageFiles(std::vector>& entries, + std::shared_ptr tree) const; + + MOBase::IPluginList::PluginStates fileState(const QString& fileName) const; + +private: + static const unsigned int PROBLEM_IMAGETYPE_UNSUPPORTED = 1; + +private: + MOBase::IOrganizer* m_MOInfo; + + bool allowAnyFile() const; + bool checkDisabledMods() const; + + bool m_InstallerUsed; + QString m_Url; +}; + +#endif // INSTALLERFOMOD_H diff --git a/libs/installer_fomod/src/resources.qrc b/libs/installer_fomod/src/resources.qrc new file mode 100644 index 0000000..19f88bf --- /dev/null +++ b/libs/installer_fomod/src/resources.qrc @@ -0,0 +1,7 @@ + + + resources/CloseButtonIcon.png + resources/LeftButtonIcon.png + resources/RightButtonIcon.png + + diff --git a/libs/installer_fomod/src/resources/CloseButtonIcon.png b/libs/installer_fomod/src/resources/CloseButtonIcon.png new file mode 100644 index 0000000..95c8149 Binary files /dev/null and b/libs/installer_fomod/src/resources/CloseButtonIcon.png differ diff --git a/libs/installer_fomod/src/resources/LeftButtonIcon.png b/libs/installer_fomod/src/resources/LeftButtonIcon.png new file mode 100644 index 0000000..181ca52 Binary files /dev/null and b/libs/installer_fomod/src/resources/LeftButtonIcon.png differ diff --git a/libs/installer_fomod/src/resources/RightButtonIcon.png b/libs/installer_fomod/src/resources/RightButtonIcon.png new file mode 100644 index 0000000..bd8d5b7 Binary files /dev/null and b/libs/installer_fomod/src/resources/RightButtonIcon.png differ diff --git a/libs/installer_fomod/src/scalelabel.cpp b/libs/installer_fomod/src/scalelabel.cpp new file mode 100644 index 0000000..e654bf5 --- /dev/null +++ b/libs/installer_fomod/src/scalelabel.cpp @@ -0,0 +1,107 @@ +#include "scalelabel.h" + +#include + +static bool isResourceMovie(const QString& path) +{ + for (QByteArray format : QMovie::supportedFormats()) { + QString fileExtension = "." + QString::fromUtf8(format); + if (path.endsWith(fileExtension)) { + return true; + } + } + + return false; +} + +ScaleLabel::ScaleLabel(QWidget* parent) : QLabel(parent) {} + +void ScaleLabel::setScalableResource(const QString& path) +{ + if (auto m = movie()) { + setMovie(nullptr); + delete m; + m_OriginalMovieSize = QSize(); + } + if (!pixmap().isNull()) { + setPixmap(QPixmap()); + m_UnscaledImage = QImage(); + } + + if (path.isEmpty()) { + return; + } + + if (isResourceMovie(path)) { + setScalableMovie(path); + } else { + setScalableImage(path); + } +} + +void ScaleLabel::setStatic(bool isStatic) +{ + m_isStatic = isStatic; + + if (auto m = movie()) { + if (isStatic) { + m->stop(); + } else { + m->start(); + } + } +} + +void ScaleLabel::setScalableMovie(const QString& path) +{ + QMovie* m = new QMovie(path); + if (!m->isValid()) { + qWarning(">%s< is an invalid movie. Reason: %s", qUtf8Printable(path), + m->lastErrorString().toStdString().c_str()); + delete m; + return; + } + + m->setParent(this); + setMovie(m); + m->start(); + m->stop(); + m_OriginalMovieSize = m->currentImage().size(); + + m->setScaledSize(m_OriginalMovieSize.scaled(size(), Qt::KeepAspectRatio)); + if (!m_isStatic) { + m->start(); + } +} + +void ScaleLabel::setScalableImage(const QString& path) +{ + QImage image(path); + if (image.isNull()) { + qWarning(">%s< is a null image", qUtf8Printable(path)); + } else { + m_UnscaledImage = image; + setPixmap(QPixmap::fromImage(image).scaled(size(), Qt::KeepAspectRatio)); + } +} + +void ScaleLabel::resizeEvent(QResizeEvent* event) +{ + if (auto m = movie()) { + m->stop(); + m->setScaledSize(m_OriginalMovieSize.scaled(event->size(), Qt::KeepAspectRatio)); + m->start(); + + // We can't just skip the start() above since that is what triggers the label to + // resize the movie The only way to resize the movie but keep it paused is to start + // and then re-stop it + if (m_isStatic) { + m->stop(); + } + } + auto p = pixmap(); + if (!p.isNull()) { + setPixmap( + QPixmap::fromImage(m_UnscaledImage).scaled(event->size(), Qt::KeepAspectRatio)); + } +} diff --git a/libs/installer_fomod/src/scalelabel.h b/libs/installer_fomod/src/scalelabel.h new file mode 100644 index 0000000..4e943c3 --- /dev/null +++ b/libs/installer_fomod/src/scalelabel.h @@ -0,0 +1,31 @@ +#ifndef SCALELABEL_H +#define SCALELABEL_H + +#include +#include +#include + +class ScaleLabel : public QLabel +{ + Q_OBJECT +public: + explicit ScaleLabel(QWidget* parent = nullptr); + + void setScalableResource(const QString& path); + void setStatic(bool isStatic); +signals: + +public slots: +protected: + virtual void resizeEvent(QResizeEvent* event); + +private: + void setScalableMovie(const QString& path); + void setScalableImage(const QString& path); + + QImage m_UnscaledImage; + QSize m_OriginalMovieSize; + bool m_isStatic = false; +}; + +#endif // SCALELABEL_H diff --git a/libs/installer_fomod/src/xmlreader.cpp b/libs/installer_fomod/src/xmlreader.cpp new file mode 100644 index 0000000..7f53710 --- /dev/null +++ b/libs/installer_fomod/src/xmlreader.cpp @@ -0,0 +1,83 @@ +#include "xmlreader.h" + +#include + +#include + +using MOBase::Exception; + +bool XmlReader::getNextElement(QString const& start) +{ + while (!atEnd()) { + switch (readNext()) { + case EndElement: + if (name() != start) { + qWarning() << "Got end of " << name() << ", expected " << start << " at " + << lineNumber(); + continue; + } + return false; + + case StartElement: + return true; + + case Invalid: + throw Exception("bad xml"); + + default: + qWarning() << "Unexpected token type " << tokenString() << " at " << lineNumber(); + } + } + return false; +} + +void XmlReader::unexpected() +{ + qWarning() << "Unexpected element " << name() << " near line " << lineNumber(); + // Eat the contents + QString s = readElementText(IncludeChildElements); + // Print them out if in debugging mode + qDebug() << " contains " << s; +} + +void XmlReader::finishedElement() +{ + QString const self = name().toString(); + while (!atEnd()) { + switch (readNext()) { + case EndElement: + if (name() != self) { + qWarning() << "Got end element for " << name() << ", expected " << self + << " at " << lineNumber(); + continue; + } + return; + + case Invalid: + throw Exception("bad xml"); + return; + + case StartElement: + unexpected(); + break; + + default: + qWarning() << "Unexpected token type " << tokenString() << " at " << lineNumber(); + } + } +} + +QString XmlReader::getText() +{ + // This reads the text in an element, leaving you at the next element. + QString result; + while (QXmlStreamReader::readNext() == Comment || tokenType() == Characters) { + if (tokenType() == Characters) { + result += text(); + } + } + if (tokenType() != EndElement) { + qWarning() << "Unexpected token type " << tokenString() << " at " << lineNumber(); + } + return result; +} diff --git a/libs/installer_fomod/src/xmlreader.h b/libs/installer_fomod/src/xmlreader.h new file mode 100644 index 0000000..974d3a1 --- /dev/null +++ b/libs/installer_fomod/src/xmlreader.h @@ -0,0 +1,40 @@ +#ifndef XMLREADER_H +#define XMLREADER_H + +#include + +class XmlReader : public QXmlStreamReader +{ +public: + XmlReader(QIODevice* device) : QXmlStreamReader(device) {} + + XmlReader(QByteArray array) : QXmlStreamReader(array) {} + + /** Get the next token, ignoring comments and white space text */ + TokenType readNext() + { + while (QXmlStreamReader::readNext() == Comment || isWhitespace()) { + continue; + } + return tokenType(); + } + + /** get the next element. + * + * \param start - the name of the current start element + * + * \returns false if no more elements + */ + bool getNextElement(QString const& start); + + /* Get the text associated with this token. */ + QString getText(); + + /** Print a message if we get an unexpected tag */ + void unexpected(); + + /** Read till the end of an element. Used for leaf nodes */ + void finishedElement(); +}; + +#endif // XMLREADER_H diff --git a/libs/installer_fomod/vcpkg.json b/libs/installer_fomod/vcpkg.json new file mode 100644 index 0000000..28051ad --- /dev/null +++ b/libs/installer_fomod/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673" + } + } +} diff --git a/libs/installer_fomod_csharp/.clang-format b/libs/installer_fomod_csharp/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/installer_fomod_csharp/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/installer_fomod_csharp/.git-blame-ignore-revs b/libs/installer_fomod_csharp/.git-blame-ignore-revs new file mode 100644 index 0000000..a26d6d2 --- /dev/null +++ b/libs/installer_fomod_csharp/.git-blame-ignore-revs @@ -0,0 +1 @@ +f45b25a7b347ae5a42ca86ae60a47b177e9caf6e diff --git a/libs/installer_fomod_csharp/.gitattributes b/libs/installer_fomod_csharp/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/installer_fomod_csharp/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/installer_fomod_csharp/.github/workflows/build.yml b/libs/installer_fomod_csharp/.github/workflows/build.yml new file mode 100644 index 0000000..b9d11e3 --- /dev/null +++ b/libs/installer_fomod_csharp/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build Installer FOMOD C# + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Installer FOMOD C# + id: build-installer-fomod-csharp + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase diff --git a/libs/installer_fomod_csharp/.github/workflows/linting.yml b/libs/installer_fomod_csharp/.github/workflows/linting.yml new file mode 100644 index 0000000..80d5fd4 --- /dev/null +++ b/libs/installer_fomod_csharp/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Installer FOMOD C# Plugin + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/installer_fomod_csharp/.gitignore b/libs/installer_fomod_csharp/.gitignore new file mode 100644 index 0000000..58b3448 --- /dev/null +++ b/libs/installer_fomod_csharp/.gitignore @@ -0,0 +1 @@ +vsbuild diff --git a/libs/installer_fomod_csharp/.pre-commit-config.yaml b/libs/installer_fomod_csharp/.pre-commit-config.yaml new file mode 100644 index 0000000..3103a1f --- /dev/null +++ b/libs/installer_fomod_csharp/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v19.1.5 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/installer_fomod_csharp/CMakeLists.txt b/libs/installer_fomod_csharp/CMakeLists.txt new file mode 100644 index 0000000..3170ab3 --- /dev/null +++ b/libs/installer_fomod_csharp/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(installer_fomod_csharp) + +add_subdirectory(src) diff --git a/libs/installer_fomod_csharp/CMakePresets.json b/libs/installer_fomod_csharp/CMakePresets.json new file mode 100644 index 0000000..5a63c77 --- /dev/null +++ b/libs/installer_fomod_csharp/CMakePresets.json @@ -0,0 +1,67 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/installer_fomod_csharp/LICENSE b/libs/installer_fomod_csharp/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/libs/installer_fomod_csharp/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/libs/installer_fomod_csharp/README.md b/libs/installer_fomod_csharp/README.md new file mode 100644 index 0000000..6b4e4d1 --- /dev/null +++ b/libs/installer_fomod_csharp/README.md @@ -0,0 +1,20 @@ +# [MO2](https://github.com/ModOrganizer2/modorganizer) plugin for FOMOD installers using C# scripts + +This is simple installer plugin for Mod Organizer 2 to handle FOMOD installers containing C# script. + +Without this plugin, Mod Organizer 2 will use NCC to install such plugins, but NCC has a few limitations when +used within MO2: + +- Mod detection will not work, so the installer will not be able to check for active mods or even existing files. +- Modifying settings (`FalloutNV.ini`, etc.) does not work properly with the NCC installer (it may work if you + do not use per-profile settings, but that is not recommended). + +This installer is better integrated with Mod Organizer 2 and should handle any kind of FOMOD installer containing +a C# script. Feel free to [open an issue](https://github.com/ModOrganizer2/modorganizer/issues/new?assignees=&labels=issue+report&template=issue-report.md) +if you find a bug. + +The installer will never modify settings silently. If during installation, the script tries to modify settings, +they will be stored in memory and presented to you at the end of the installation. You will then be able to either +apply the settings, save them inside the mod folder or discard them. + +**Warning:** This plugin currently only works with Mod Organizer 2 development build 2.3.0 alpha 10! diff --git a/libs/installer_fomod_csharp/src/CMakeLists.txt b/libs/installer_fomod_csharp/src/CMakeLists.txt new file mode 100644 index 0000000..09b3015 --- /dev/null +++ b/libs/installer_fomod_csharp/src/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(mo2-cmake CONFIG REQUIRED) +find_package(mo2-uibase CONFIG REQUIRED) + +add_library(installer_fomod_csharp SHARED) +mo2_configure_plugin(installer_fomod_csharp WARNINGS OFF CLI ON) +target_link_libraries(installer_fomod_csharp PRIVATE mo2::uibase) +mo2_install_plugin(installer_fomod_csharp) + +set_target_properties(installer_fomod_csharp PROPERTIES CXX_STANDARD 20) diff --git a/libs/installer_fomod_csharp/src/base_script.cpp b/libs/installer_fomod_csharp/src/base_script.cpp new file mode 100644 index 0000000..e3aa4a7 --- /dev/null +++ b/libs/installer_fomod_csharp/src/base_script.cpp @@ -0,0 +1,685 @@ +/* +Copyright (C) 2020 Holt59. All rights reserved. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "base_script.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "csharp_interface.h" +#include "csharp_utils.h" +#include "installer_fomod_postdialog.h" +#include "psettings.h" + +using namespace MOBase; + +// clang-format off + +namespace CSharp { + + // Pointer to object: + static MOBase::IOrganizer* g_Organizer = nullptr; + + // Per-install globals: + struct Globals { + IInstallationManager* InstallManager; + QWidget* ParentWidget; + std::shared_ptr SourceTree; + std::shared_ptr DestinationTree; + + // Map from path in destination entry to the original entry: + std::map, + std::shared_ptr> InstalledEntries; + + // Map extracted entries (in the original tree) to (temporary) paths: + std::map, QString> ExtractedEntries; + + // Map creted entries (in the destination tree) to (temporary) paths: + std::map, QString> CreatedEntries; + + // List of modified settings values: + std::map Settings; + + Globals() { } + Globals( + IPlugin const* plugin, MOBase::IInstallationManager* manager, QWidget* parentWidget, + std::shared_ptr tree, std::map, QString> entries) : + m_Plugin(plugin), InstallManager(manager), ParentWidget(parentWidget), SourceTree(tree), DestinationTree(tree->createOrphanTree()), ExtractedEntries(std::move(entries)) { + + } + + Globals(Globals const&) = delete; + Globals(Globals&&) = default; + + Globals& operator=(Globals const&) = delete; + Globals& operator=(Globals&&) = default; + + private: + IPlugin const* m_Plugin; + }; + static Globals g; + + + + void init(MOBase::IOrganizer* moInfo) { + + // Do this only once: + if (g_Organizer == nullptr) { + Application::EnableVisualStyles(); + Application::SetCompatibleTextRenderingDefault(false); + } + + g_Organizer = moInfo; + } + + void beforeInstall(IPlugin const* plugin, MOBase::IInstallationManager* manager, QWidget* parentWidget, + std::shared_ptr tree, std::map, QString> entries) { + g = { plugin, manager, parentWidget, tree, std::move(entries) }; + } + + IPluginInstaller::EInstallResult postInstall(std::shared_ptr& tree) { + + if (!g.Settings.empty()) { + + InstallerFomodPostDialog* dialog = new InstallerFomodPostDialog(g.ParentWidget); + + dialog->setIniSettings(g.Settings); + + // Installation cancelled: + if (dialog->exec() == QDialog::Rejected) { + return IPluginInstaller::EInstallResult::RESULT_CANCELED; + } + + switch (dialog->result()) { + + // Discard, nothing do to: + case InstallerFomodPostDialog::Result::DISCARD: break; + + // Apply, must fetch the profile INI settings and apply the settings: + case InstallerFomodPostDialog::Result::APPLY: { + for (auto& p : g.Settings) { + QDir path(g_Organizer->profilePath()); + if (!g_Organizer->profile()->localSettingsEnabled()) { + path = QDir(g_Organizer->managedGame()->documentsDirectory()); + } + + QSettings settings(path.filePath(p.first), QSettings::IniFormat); + + if (settings.status() != QSettings::NoError) { + return IPluginInstaller::EInstallResult::RESULT_FAILED; + } + + p.second.update(settings); + } + } break; + + // Move, must create the INI files and apply the settings: + case InstallerFomodPostDialog::Result::MOVE: { + for (auto& p : g.Settings) { + auto e = g.DestinationTree->addFile("INI Tweaks/" + p.first, false); + if (e == nullptr) { + return IPluginInstaller::EInstallResult::RESULT_FAILED; + } + QString path = g.InstallManager->createFile(e); + if (path.isEmpty()) { + return IPluginInstaller::EInstallResult::RESULT_FAILED; + } + QSettings settings(path, QSettings::IniFormat); + if (settings.status() != QSettings::NoError) { + return IPluginInstaller::EInstallResult::RESULT_FAILED; + } + p.second.update(settings); + } + } break; + } + + } + + tree = g.DestinationTree; + + // Clear up: + g = Globals(); + + + return IPluginInstaller::EInstallResult::RESULT_SUCCESS; + } + +} + +namespace CSharp { + + using namespace System::IO; + + bool isFomodEntry(std::shared_ptr entry) { + return entry->pathFrom(g.SourceTree).compare("fomod", Qt::CaseInsensitive) == 0; + } + + bool BaseScriptImpl::PerformBasicInstall() { + for (auto e : *g.SourceTree) { + if (!isFomodEntry(e)) { + auto ce = g.DestinationTree->copy(e, "", IFileTree::InsertPolicy::MERGE); + g.InstalledEntries[ce] = e; + } + } + return true; + } + + bool BaseScriptImpl::InstallFileFromMod(String^ p_strFrom, String^ p_strTo) { + auto sourceEntry = g.SourceTree->find(to_qstring(p_strFrom)); + + if (!sourceEntry) { + log::warn("File '{}' not found in the archive.", to_wstring(p_strFrom)); + return false; + } + + if (auto ce = g.DestinationTree->copy(sourceEntry, to_qstring(p_strTo)); ce != nullptr) { + g.InstalledEntries[ce] = sourceEntry; + return true; + } + + return false; + } + + array^ BaseScriptImpl::GetModFileList() { + // Cannot directly fill a, e.g., List^ because I cannot capture it: + std::vector paths; + g.SourceTree->walk([&](QString const& path, std::shared_ptr entry) { + // Discard fomod folder: + if (isFomodEntry(entry)) { + return IFileTree::WalkReturn::SKIP; + } + if (entry->isFile()) { + paths.push_back(path + entry->name()); + } + return IFileTree::WalkReturn::CONTINUE; + }, "/"); + + // Convert to C#: + const auto size = static_cast(paths.size()); + array^ result = gcnew array(size); + for (int i = 0; i < size; ++i) { + result[i] = from_string(paths[i].toStdWString()); + } + + return result; + } + + + /** + * @brief Extract the given entry. + * + * If the entry has already been extracted, the existing paths is returned. + * + * @param entry The entry to extract. + * + * @return path to the temporary file corresponding to the entry. + */ + QString extractFile(std::shared_ptr entry) { + + QString qPath; + if (auto it = g.ExtractedEntries.find(entry); it != g.ExtractedEntries.end()) { + qPath = it->second; + } + else { + qPath = g.InstallManager->extractFile(entry, true); + + if (qPath.isEmpty()) { + return QString(); + } + + g.ExtractedEntries[entry] = qPath; + } + + return qPath; + } + + array^ BaseScriptImpl::GetFileFromMod(String^ p_strFile) { + auto entry = g.SourceTree->find(to_qstring(p_strFile)); + + if (!entry) { + return gcnew array(0); + } + + QString qPath = extractFile(entry); + if (qPath.isEmpty()) { + return gcnew array(0); + } + + String^ path = from_string(qPath.toStdWString()); + return File::ReadAllBytes(path); + } + + QStringList getDataFiles(QString folder, QString pattern, bool allFolders) { + QStringList files = g_Organizer->findFiles(folder, [pattern](QString const& filepath) { + return QDir::match(pattern, QFileInfo(filepath).fileName()); + }); + if (allFolders) { + QStringList directories = g_Organizer->listDirectories(folder); + for (QString directory : directories) { + // MO2 does not like path with . or / (I think), so creating the path manually: + files.append(getDataFiles((folder.isEmpty() ? "" : folder + QDir::separator()) + directory, pattern, allFolders)); + } + } + return files; + } + + array^ BaseScriptImpl::GetExistingDataFileList(String^ p_strPath, String^ p_strPattern, bool p_booAllFolders) { + QStringList files = getDataFiles(to_qstring(p_strPath), to_qstring(p_strPattern), p_booAllFolders); + array^ result = gcnew array(files.size()); + + QDir modsDir(g_Organizer->modsPath()); + for (int i = 0; i < files.size(); ++i) { + // We need to trim the path to the actual folder containing the mod (did not find a better way): + QString rpath = modsDir.relativeFilePath(files[i]).replace("/", QDir::separator()); + result[i] = from_string(rpath.mid(rpath.indexOf(QDir::separator()) + 1).toStdWString()); + } + return result; + } + + /** + * @brief Find the data-file path corresponding to the given path. + * + * This methods first lookup the file in the destination tree of the mod (for files + * that have been extracted / created by the mod), and if it does not find it there + * lookup files from other mods. + * + * @param p_strPath Path to the file to lookup, relative to the data folder. + * + * @return a path to an actual corresponding file, or a null pointer if the + * file was not found. + */ + String^ getDataFilePath(String^ p_strPath) { + + // Check if the file is in the output tree: + QString qPath = to_qstring(p_strPath); + if (auto e = g.DestinationTree->find(qPath); e != nullptr) { + + // Check if it's a created entry: + if (auto it = g.CreatedEntries.find(e); it != g.CreatedEntries.end()) { + return from_string(g.CreatedEntries[e]); + } + + // Find the source entry - We need to check for parent: + std::shared_ptr originalEntry; + if (auto it = g.InstalledEntries.find(e); it != g.InstalledEntries.end()) { + originalEntry = it->second; + } + else { + std::shared_ptr tree = e->parent(); + decltype(tree) oTree = nullptr; + while (oTree == nullptr && tree != nullptr) { + auto it = g.InstalledEntries.find(tree); + if (it != g.InstalledEntries.end()) { + oTree = it->second->astree(); + } + else { + tree = tree->parent(); + } + } + originalEntry = oTree->find(e->pathFrom(tree)); + } + QString path = extractFile(originalEntry); + if (path.isEmpty()) { + return nullptr; + } + return from_string(path); + } + + // Convert to path and normalize separator: + auto path = std::filesystem::path(qPath.toStdWString()).make_preferred(); + QStringList paths = g_Organizer->findFiles(ToQString(path.parent_path().native()), [name = ToQString(path.filename())](QString const& filepath) { + return QFileInfo(filepath).fileName().compare(name, Qt::CaseInsensitive) == 0; + }); + + if (paths.isEmpty()) { + return nullptr; + } + + return from_string(paths[0]); + } + + bool BaseScriptImpl::DataFileExists(String^ p_strPath) { + return getDataFilePath(p_strPath) != nullptr; + } + + array^ BaseScriptImpl::GetExistingDataFile(String^ p_strPath) { + + // Convert to path and normalize separator: + String^ datapath = getDataFilePath(p_strPath); + + if (datapath == nullptr) { + return nullptr; + } + + // Read the first file (should be only one): + return File::ReadAllBytes(datapath); + } + + bool BaseScriptImpl::GenerateDataFile(String^ p_strPath, array^ p_bteData) { + + // Check if we already have created an entry for this: + QString qPath = to_qstring(p_strPath); + auto entry = g.DestinationTree->find(qPath); + + QString qAbsPath; + // If the entry is in the list of created files (note: if the entry does not exist, + // find return a nullptr, which is never in g.CreatedEntries). + if (auto it = g.CreatedEntries.find(entry); it != g.CreatedEntries.end()) { + qAbsPath = g.CreatedEntries.at(entry); + } + // Otherwize: Create the entry and the temporary file: + else { + entry = g.DestinationTree->addFile(qPath, true); + qAbsPath = g.InstallManager->createFile(entry); + if (qAbsPath.isEmpty()) { + // Remove the entry from the tree: + entry->detach(); + return false; + } + + // Store the created entry: + g.CreatedEntries[entry] = qAbsPath; + } + + String^ absPath = from_string(qAbsPath); + File::WriteAllBytes(absPath, p_bteData); + return true; + } + + // UI methods: + + DialogResult BaseScriptImpl::ExtendedMessageBox(String^ p_strMessage, String^ p_strTitle, String^ p_strDetails, MessageBoxButtons p_mbbButtons, MessageBoxIcon p_mdiIcon) { + QMessageBox messageBox(g.ParentWidget); + if (!String::IsNullOrEmpty(p_strTitle)) { + messageBox.setWindowTitle(to_qstring(p_strTitle)); + } + messageBox.setText(to_qstring(p_strMessage)); + + if (!String::IsNullOrEmpty(p_strDetails)) { + messageBox.setDetailedText(to_qstring(p_strDetails)); + } + + // For whatever reason MessageBoxIcon has duplicated entries... + switch (p_mdiIcon) { + case MessageBoxIcon::Error: + // case MessageBoxIcon::Stop: + messageBox.setIcon(QMessageBox::Icon::Critical); + break; + case MessageBoxIcon::Asterisk: + // case MessageBoxIcon::Information: + messageBox.setIcon(QMessageBox::Icon::Information); + break; + case MessageBoxIcon::Question: + messageBox.setIcon(QMessageBox::Icon::Question); + break; + case MessageBoxIcon::Exclamation: + // case MessageBoxIcon::Hand: + // case MessageBoxIcon::Warning: + // case MessageBoxIcon::None: + messageBox.setIcon(QMessageBox::Icon::Warning); + case MessageBoxIcon::None: + messageBox.setIcon(QMessageBox::Icon::NoIcon); + break; + } + + QMessageBox::StandardButtons buttons; + + switch (p_mbbButtons) { + case MessageBoxButtons::AbortRetryIgnore: + buttons = QMessageBox::StandardButton::Abort | QMessageBox::StandardButton::Retry | QMessageBox::StandardButton::Ignore; + break; + case MessageBoxButtons::OK: + buttons = QMessageBox::StandardButton::Ok; + break; + case MessageBoxButtons::OKCancel: + buttons = QMessageBox::StandardButton::Ok | QMessageBox::Cancel; + break; + case MessageBoxButtons::RetryCancel: + buttons = QMessageBox::StandardButton::Cancel | QMessageBox::StandardButton::Retry; + break; + case MessageBoxButtons::YesNoCancel: + buttons = QMessageBox::StandardButton::Yes | QMessageBox::No | QMessageBox::Cancel; + break; + case MessageBoxButtons::YesNo: + buttons = QMessageBox::StandardButton::Yes | QMessageBox::No; + break; + } + + messageBox.setStandardButtons(buttons); + + // Only some case are possible here: + switch (messageBox.exec()) { + case QMessageBox::Button::Abort: + return DialogResult::Abort; + case QMessageBox::Button::Cancel: + return DialogResult::Cancel; + case QMessageBox::Button::Ignore: + return DialogResult::Ignore; + case QMessageBox::Button::No: + return DialogResult::No; + case QMessageBox::Button::Ok: + return DialogResult::OK; + case QMessageBox::Button::Retry: + return DialogResult::Retry; + case QMessageBox::Button::Yes: + return DialogResult::Yes; + } + + return DialogResult::None; + } + + + array^ BaseScriptImpl::Select(array^ p_sopOptions, String^ p_strTitle, bool p_booSelectMany) { + using namespace System::Collections::Generic; + + QDialog* inputDialog = new QDialog(); + QVBoxLayout* layout = new QVBoxLayout(inputDialog); + inputDialog->setWindowTitle(to_qstring(p_strTitle)); + inputDialog->setLayout(layout); + + layout->setSizeConstraint(QLayout::SetFixedSize); + + if (p_booSelectMany) { + layout->addWidget(new QLabel(QObject::tr("Choose any:"), inputDialog)); + } + else { + layout->addWidget(new QLabel(QObject::tr("Choose one:"), inputDialog)); + } + + QList items; + for each (SelectOption ^ opt in p_sopOptions) { + QAbstractButton* btn; + if (p_booSelectMany) { + btn = new QCheckBox(to_qstring(opt->Item), inputDialog); + } + else { + btn = new QRadioButton(to_qstring(opt->Item), inputDialog); + } + + if (!String::IsNullOrEmpty(opt->Desc)) { + btn->setToolTip(to_qstring(opt->Desc)); + } + + layout->addWidget(btn); + items.append(btn); + } + + if (!p_booSelectMany && items.size() > 0) { + items[0]->setChecked(true); + } + + QDialogButtonBox* buttonBox = new QDialogButtonBox( + QDialogButtonBox::Cancel | QDialogButtonBox::Ok, inputDialog); + layout->addWidget(buttonBox); + + // Using old signal/slot syntax since the new one does not work here (probably + // due to the C++/CLR nature): + QObject::connect(buttonBox, SIGNAL(accepted()), inputDialog, SLOT(accept())); + QObject::connect(buttonBox, SIGNAL(rejected()), inputDialog, SLOT(reject())); + + inputDialog->setModal(true); + if (inputDialog->exec() != QDialog::Accepted) { + return gcnew array(0); + } + + List^ selected = gcnew List(items.size()); + for (int i = 0; i < items.size(); ++i) { + if (items[i]->isChecked()) { + selected->Add(i); + } + } + + return selected->ToArray(); + } + + // Versioning / INIs: + + // Convert to Version^ from a MO2 version: + inline System::Version^ make_version(VersionInfo version) { + auto qversion = version.asQVersionNumber(); + return gcnew System::Version(qversion.majorVersion(), qversion.minorVersion(), qversion.microVersion()); + + } + + System::Version^ BaseScriptImpl::GetModManagerVersion() { + const auto version = g_Organizer->version(); + return gcnew System::Version(version.major(), version.minor(), version.patch()); + } + + System::Version^ BaseScriptImpl::GetGameVersion() { + return make_version(g_Organizer->managedGame()->gameVersion()); + } + + System::Version^ BaseScriptImpl::GetScriptExtenderVersion() { + auto scriptExtender = g_Organizer->gameFeatures()->gameFeature(); + + if (!scriptExtender || !scriptExtender->isInstalled()) { + return nullptr; + } + + return gcnew System::Version(msclr::interop::marshal_as(scriptExtender->getExtenderVersion().toStdString())); + } + + bool BaseScriptImpl::ScriptExtenderPresent() { + auto scriptExtender = g_Organizer->gameFeatures()->gameFeature(); + return scriptExtender && scriptExtender->isInstalled(); + } + + // Plugins: + array^ BaseScriptImpl::GetAllPlugins() { + QStringList names = g_Organizer->pluginList()->pluginNames(); + array^ result = gcnew array(names.size()); + for (int i = 0; i < names.size(); ++i) { + result[i] = from_string(names[i].toStdString()); + } + return result; + } + + array^ BaseScriptImpl::GetActivePlugins() { + auto pluginList = g_Organizer->pluginList(); + QStringList names = pluginList->pluginNames(); + QStringList activeNames; + for (auto& name : names) { + if (pluginList->state(name) == IPluginList::STATE_ACTIVE) { + activeNames.append(name); + } + } + array^ result = gcnew array(activeNames.size()); + for (int i = 0; i < activeNames.size(); ++i) { + result[i] = from_string(activeNames[i].toStdString()); + } + return result; + } + + // INIs: + String^ BaseScriptImpl::GetIniString(String^ settingsFileName, String^ section, String^ key) { + + // Check if we have already set this within this installation: + auto fIt = g.Settings.find(to_qstring(settingsFileName)); + if (fIt != g.Settings.end()) { + QString value = fIt->second.value(to_qstring(section), to_qstring(key)); + if (!value.isEmpty()) { + return from_string(value); + } + } + + // Otherwize, look-up the file: + QDir path(g_Organizer->profilePath()); + if (!g_Organizer->profile()->localSettingsEnabled()) { + path = QDir(g_Organizer->managedGame()->documentsDirectory()); + } + + QSettings settings(path.filePath(to_qstring(settingsFileName)), QSettings::IniFormat); + + if (settings.status() != QSettings::NoError) { + return nullptr; + } + + QString name = to_qstring(section + "/" + key); + if (section->Equals("General", System::StringComparison::CurrentCultureIgnoreCase)) { + name = to_qstring(key); + } + + QVariant value = settings.value(name); + if (!value.isValid()) { + return nullptr; + } + + return from_string(value.toString().toStdString()); + } + + int BaseScriptImpl::GetIniInt(String^ settingsFileName, String^ section, String^ key) { + return Convert::ToInt32(GetIniString(settingsFileName, section, key)); + } + + bool BaseScriptImpl::EditIni(String^ p_strSettingsFileName, String^ p_strSection, String^ p_strKey, String^ p_strValue) { + // Check that the file is supported: + bool iniFound = false; + for (auto ini : g_Organizer->managedGame()->iniFiles()) { + if (ini.compare(to_qstring(p_strSettingsFileName), Qt::CaseInsensitive) == 0) { + iniFound = true; + } + } + + if (!iniFound) { + return false; + } + + g.Settings[to_qstring(p_strSettingsFileName)].setValue(to_qstring(p_strSection), to_qstring(p_strKey), to_qstring(p_strValue)); + return true; + } + +} diff --git a/libs/installer_fomod_csharp/src/base_script.h b/libs/installer_fomod_csharp/src/base_script.h new file mode 100644 index 0000000..abfa152 --- /dev/null +++ b/libs/installer_fomod_csharp/src/base_script.h @@ -0,0 +1,552 @@ +/* +Copyright (C) 2020 Holt59. All rights reserved. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +// clang-format off + +#ifndef BASE_SCRIPT_H +#define BASE_SCRIPT_H + +#using +#using +#using + +#include + +/** + * Note: The specification of BaseScript where taken from the Nexus-Mods installer_fomod extension + * for Vortex: https://github.com/Nexus-Mods/fomod-installer + */ +namespace CSharp { + + using namespace System; + using namespace System::Drawing; + using namespace System::Windows::Forms; + + /// + /// Describes the options to display in a select form. + /// + public ref struct SelectOption { + public: + /// + /// The name of the selection item. + /// + String^ Item; + + /// + /// The path to the preview image of the item. + /// + String^ Preview; + + /// + /// The description of the selection item. + /// + String^ Desc; + + /// + /// A simple constructor that initializes the struct with the given values. + /// + /// The name of the selection item. + /// The path to the preview image of the item. + /// The description of the selection item. + SelectOption(String^ item, String^ preview, String^ desc) { + Item = item; + Preview = preview; + Desc = desc; + } + }; + + /// + /// The base class for C# scripts. + /// + public ref class BaseScriptImpl { + public: + + static String^ LastError; + + /// + /// Returns the last error that occurred. + /// + /// The last error that occurred. + static String^ GetLastError() { + return LastError; + } + + /// + /// Performs a basic install of the mod. + /// + /// + /// A basic install installs all of the file in the mod to the Data directory + /// or activates all esp and esm files. + /// + /// true if the installation succeed; + /// false otherwise. + static bool PerformBasicInstall(); + + /// + /// Installs the specified file from the mod to the specified location on the file system. + /// + /// + /// This is the legacy form of . It now just calls + /// . + /// + /// The path of the file in the mod to install. + /// The path on the file system where the file is to be created. + /// true if the file was written; false otherwise. + /// + static bool CopyDataFile(String^ p_strFrom, String^ p_strTo) { + return InstallFileFromMod(p_strFrom, p_strTo); + } + + /// + /// Installs the specified file from the mod to the specified location on the file system. + /// + /// The path of the file in the mod to install. + /// The path on the file system where the file is to be created. + /// true if the file was written; false otherwise. + static bool InstallFileFromMod(String^ p_strFrom, String^ p_strTo); + + /// + /// Installs the speified file from the mod to the file system. + /// + /// The path of the file to install. + /// true if the file was written; false otherwise. + static bool InstallFileFromMod(String^ p_strFile) { + return InstallFileFromMod(p_strFile, p_strFile); + } + + /// + /// Installs the specified file from the mod to the specified location on the file system. + /// + /// The path of the file in the mod to install. + /// The path on the file system where the file is to be created. + /// true if the file was written; false otherwise. + static bool InstallFileFromFomod(String^ p_strFrom, String^ p_strTo) { + return InstallFileFromMod(p_strFrom, p_strTo); + } + + /// + /// Installs the speified file from the mod to the file system. + /// + /// The path of the file to install. + /// true if the file was written; false otherwise. + static bool InstallFileFromFomod(String^ p_strFile) { + return InstallFileFromMod(p_strFile, p_strFile); + } + + /// + /// Retrieves the list of files in the mod. + /// + /// The list of files in the mod. + static array^ GetModFileList(); + + /// + /// Retrieves the list of files in the mod. + /// + /// The list of files in the mod. + static array^ GetFomodFileList() { + return GetModFileList(); + } + + /// + /// Retrieves the specified file from the mod. + /// + /// The file to retrieve. + /// The requested file data. + static array^ GetFileFromMod(String^ p_strFile); + + /// + /// Retrieves the specified file from the mod. + /// + /// The file to retrieve. + /// The requested file data. + static array^ GetFileFromFomod(String^ p_strFile) { + return GetFileFromMod(p_strFile); + } + + /// + /// Gets a filtered list of all files in a user's Data directory. + /// + /// The subdirectory of the Data directory from which to get the listing. + /// The pattern against which to filter the file paths. + /// Whether or not to search through subdirectories. + /// A filtered list of all files in a user's Data directory. + static array^ GetExistingDataFileList(String^ p_strPath, String^ p_strPattern, bool p_booAllFolders); + + /// + /// Determines if the specified file exists in the user's Data directory. + /// + /// The path of the file whose existence is to be verified. + /// true if the specified file exists; false + /// otherwise. + static bool DataFileExists(String^ p_strPath); + + /// + /// Gets the speified file from the user's Data directory. + /// + /// The path of the file to retrieve. + /// The specified file, or null if the file does not exist. + static array^ GetExistingDataFile(String^ p_strPath); + + /// + /// Writes the file represented by the given byte array to the given path. + /// + /// + /// This method writes the given data as a file at the given path. If the file + /// already exists the user is prompted to overwrite the file. + /// + /// The path where the file is to be created. + /// The data that is to make up the file. + /// true if the file was written; false otherwise. + static bool GenerateDataFile(String^ p_strPath, array^ p_bteData); + + /// + /// Shows a message box with the given message. + /// + /// The message to display in the message box. + static void MessageBox(String^ p_strMessage) { + MessageBox(p_strMessage, nullptr); + } + + /// + /// Shows a message box with the given message and title. + /// + /// The message to display in the message box. + /// The message box's title, display in the title bar. + static void MessageBox(String^ p_strMessage, String^ p_strTitle) { + MessageBox(p_strMessage, p_strTitle, MessageBoxButtons::OK); + } + + /// + /// Shows a message box with the given message, title, and buttons. + /// + /// The message to display in the message box. + /// The message box's title, display in the title bar. + /// The buttons to show in the message box. + static DialogResult MessageBox(String^ p_strMessage, String^ p_strTitle, MessageBoxButtons p_mbbButtons) { + return MessageBox(p_strMessage, p_strTitle, p_mbbButtons, MessageBoxIcon::Information); + } + + static DialogResult MessageBox(String^ p_strMessage, String^ p_strTitle, MessageBoxButtons p_mbbButtons, MessageBoxIcon p_mdiIcon) { + return ExtendedMessageBox(p_strMessage, p_strTitle, nullptr, p_mbbButtons, p_mdiIcon); + } + + /// + /// Shows an extended message box with the given message, title, details, buttons, and icon. + /// + /// The message to display in the message box. + /// The message box's title, displayed in the title bar. + /// The message box's details, displayed in the details area. + /// The buttons to show in the message box. + /// The icon to display in the message box. + static DialogResult ExtendedMessageBox(String^ p_strMessage, String^ p_strTitle, String^ p_strDetails, MessageBoxButtons p_mbbButtons, MessageBoxIcon p_mdiIcon); + + /// + /// Displays a selection form to the user. + /// + /// The options from which to select. + /// The title of the selection form. + /// Whether more than one item can be selected. + /// The indices of the selected items. + static array^ Select(array^ p_sopOptions, String^ p_strTitle, bool p_booSelectMany); + + /// + /// Displays a selection form to the user. + /// + /// + /// The items, previews, and descriptions are repectively ordered. In other words, + /// the i-th item in uses the i-th preview in + /// and the i-th description in . + /// + /// Similarly, the idices return as results correspond to the indices of the items in + /// . + /// + /// The items from which to select. + /// The preview image file names for the items. + /// The descriptions of the items. + /// The title of the selection form. + /// Whether more than one item can be selected. + /// The indices of the selected items. + static array^ Select(array^ p_strItems, array^ p_strPreviewPaths, array^ p_strDescriptions, String^ p_strTitle, bool p_booSelectMany) { + const int size = p_strItems->GetLength(0); + array^ options = gcnew array(size); + for (int i = 0; i < size; ++i) { + options[i] = gcnew SelectOption(p_strItems[i], p_strPreviewPaths[i], p_strDescriptions[i]); + } + return Select(options, p_strTitle, p_booSelectMany); + } + + /// + /// Displays a selection form to the user. + /// + /// + /// The items, previews, and descriptions are repectively ordered. In other words, + /// the i-th item in uses the i-th preview in + /// and the i-th description in . + /// + /// Similarly, the idices return as results correspond to the indices of the items in + /// . + /// + /// The items from which to select. + /// The preview images for the items. + /// The descriptions of the items. + /// The title of the selection form. + /// Whether more than one item can be selected. + /// The indices of the selected items. + static array^ ImageSelect(array^ p_strItems, array^ /* p_imgPreviews */, array^ p_strDescriptions, String^ p_strTitle, bool p_booSelectMany) { + return Select(p_strItems, gcnew array(p_strItems->Length), p_strDescriptions, p_strTitle, p_booSelectMany); + } + + /// + /// Creates a form that can be used in custom mod scripts. + /// + /// A form that can be used in custom mod scripts. + static Form^ CreateCustomForm() { + Form^ form = gcnew Form; + return form; + } + + /// + /// Gets the version of the mod manager. + /// + /// The version of the mod manager. + static System::Version^ GetModManagerVersion(); + + /// + /// Gets the version of the game that is installed. + /// + /// The version of the game, or null if Fallout + /// is not installed. + static System::Version^ GetGameVersion(); + + // This is not in the spec., but I do not see a point in checking each + // script extender in a different way. + static System::Version^ GetScriptExtenderVersion(); + + /// + /// Gets the script extender version or null if it's not installed + /// + /// + static System::Version^ GetSkseVersion() { + return GetScriptExtenderVersion(); + } + + /// + /// Gets the script extender version or null if it's not installed + /// + /// + static System::Version^ GetFoseVersion() { + return GetScriptExtenderVersion(); + } + + /// + /// Gets the script extender version or null if it's not installed + /// + /// + static System::Version^ GetNvseVersion() { + return GetScriptExtenderVersion(); + } + + /// + /// Gets the version of the mod manager. + /// + /// The version of the mod manager. + static System::Version^ GetFommVersion() { + return GetModManagerVersion(); + } + + /// + /// Gets the version of the game that is installed. + /// + /// The version of the game, or null if Fallout + /// is not installed. + static System::Version^ GetFalloutVersion() { + // I think this is an old function... What Fallout anyway? So just going + // to return the game version, whatever current game. + return GetGameVersion(); + } + + /// + /// Determins if the script extender for the game is installed + /// (this checks for the script extender of the game for which the + /// mod is being installed) + /// + /// + static bool ScriptExtenderPresent(); + + /// + /// Gets a list of all install plugins. + /// + /// A list of all install plugins. + static array^ GetAllPlugins(); + + /// + /// Retrieves a list of currently active plugins. + /// + /// A list of currently active plugins. + static array^ GetActivePlugins(); + + /// + /// Sets the activated status of a plugin (i.e., and esp or esm file). + /// + /// The path to the plugin to activate or deactivate. + /// Whether to activate the plugin. + static void SetPluginActivation(String^ /* p_strPluginPath */, bool /* p_booActivate */) { + // throw gcnew NotImplementedException("SetPluginActivation"); + } + + /// + /// Sets the load order of the specifid plugin. + /// + /// The path to the plugin file whose load order is to be set. + /// The new load order index of the plugin. + static void SetPluginOrderIndex(String^ /* p_strPlugin */, int /* p_intNewIndex */) { + // throw gcnew NotImplementedException("SetPluginOrderIndex"); + } + + /// + /// Sets the load order of the plugins. + /// + /// + /// Each plugin will be moved from its current index to its indices' position + /// in . + /// + /// The new load order of the plugins. Each entry in this array + /// contains the current index of a plugin. This array must contain all current indices. + static void SetLoadOrder(array^ /* p_intPlugins */) { + // throw gcnew NotImplementedException("SetLoadOrder"); + } + + /// + /// Moves the specified plugins to the given position in the load order. + /// + /// + /// Note that the order of the given list of plugins is not maintained. They are re-ordered + /// to be in the same order as they are in the before-operation load order. This, I think, + /// is somewhat counter-intuitive and may change, though likely not so as to not break + /// backwards compatibility. + /// + /// The list of plugins to move to the given position in the + /// load order. Each entry in this array contains the current index of a plugin. + /// The position in the load order to which to move the specified + /// plugins. + static void SetLoadOrder(array^ /* p_intPlugins */, int /* p_intPosition */) { + // throw gcnew NotImplementedException("SetLoadOrder"); + } + + /// + /// Retrieves the specified settings value as a string. + /// + /// The name of the settings file from which to retrieve the value. + /// The section containing the value to retrieve. + /// The key of the value to retrieve. + /// The specified value as a string. + static String^ GetIniString(String^ settingsFileName, String^ section, String^ key); + + /// + /// Retrieves the specified settings value as an integer. + /// + /// The name of the settings file from which to retrieve the value. + /// The section containing the value to retrieve. + /// The key of the value to retrieve. + /// The specified value as an integer. + static int GetIniInt(String^ settingsFileName, String^ section, String^ key); + + /// + /// Retrieves the specified Fallout.ini value as a string. + /// + /// The section containing the value to retrieve. + /// The key of the value to retrieve. + /// The specified value as a string. + /// + static String^ GetFalloutIniString(String^ section, String^ key) { + return GetIniString("Fallout.ini", section, key); + } + + /// + /// Retrieves the specified Fallout.ini value as an integer. + /// + /// The section containing the value to retrieve. + /// The key of the value to retrieve. + /// + static int GetFalloutIniInt(String^ section, String^ key) { + return GetIniInt("Fallout.ini", section, key); + } + + /// + /// Retrieves the specified FalloutPrefs.ini value as a string. + /// + /// The section containing the value to retrieve. + /// The key of the value to retrieve. + /// The specified value as a string. + /// + static String^ GetPrefsIniString(String^ p_strSection, String^ p_strKey) { + // This looks wrong? Yes! But that's what used in other mod managers... + return GetIniString("FalloutPrefs.ini", p_strSection, p_strKey); + } + + /// + /// Retrieves the specified FalloutPrefs.ini value as an integer. + /// + /// The section containing the value to retrieve. + /// The key of the value to retrieve. + /// The specified value as an integer. + /// + static int GetPrefsIniInt(String^ p_strSection, String^ p_strKey) { + // This looks wrong? Yes! But that's what used in other mod managers... + return GetIniInt("FalloutPrefs.ini", p_strSection, p_strKey); + } + + /// + /// Sets the specified value in the specified Ini file to the given value. + /// + /// The name of the settings file to edit. + /// The section in the Ini file to edit. + /// The key in the Ini file to edit. + /// The value to which to set the key. + /// true if the value was set; false + /// if the user chose not to overwrite the existing value. + static bool EditIni(String^ p_strSettingsFileName, String^ p_strSection, String^ p_strKey, String^ p_strValue); + + /// + /// Sets the specified value in the Fallout.ini file to the given value. + /// + /// The section in the Ini file to edit. + /// The key in the Ini file to edit. + /// The value to which to set the key. + /// Not used. + /// true if the value was set; false + /// if the user chose not to overwrite the existing value. + static bool EditFalloutINI(String^ p_strSection, String^ p_strKey, String^ p_strValue, bool /* p_booSaveOld */) { + return EditIni("Fallout.ini", p_strSection, p_strKey, p_strValue); + } + + }; + + /** + * @brief Post-install script. + */ + MOBase::IPluginInstaller::EInstallResult postInstall(std::shared_ptr& tree); +} + +// BaseScript cannot be in a namespace: +public ref struct SelectOption: public CSharp::SelectOption { + SelectOption(System::String^ item, System::String^ preview, System::String^ desc) : + CSharp::SelectOption(item, preview, desc) { } +}; +public ref class BaseScript: public CSharp::BaseScriptImpl { }; + +#endif diff --git a/libs/installer_fomod_csharp/src/csharp_interface.cpp b/libs/installer_fomod_csharp/src/csharp_interface.cpp new file mode 100644 index 0000000..91a4a0d --- /dev/null +++ b/libs/installer_fomod_csharp/src/csharp_interface.cpp @@ -0,0 +1,157 @@ +#include "csharp_interface.h" + +#include +#include +#include + +#include + +#include "base_script.h" +#include "csharp_utils.h" + +// clang-format off + +#using + +using namespace MOBase; + +/** + * This is a assembly resolve handler that does only one thing: returns the assembly + * containing BaseScript (usually the DLL) when requested. + * + * I don't know why this must be done manually... But I did not find any better solution. + */ +System::Reflection::Assembly^ currentDomain_AssemblyResolve(System::Object^, System::ResolveEventArgs^ args) +{ + using namespace System::Reflection; + + try { + Assembly^ baseScriptAssembly = System::Reflection::Assembly::GetAssembly(BaseScript::typeid); + if (args->Name->Equals(baseScriptAssembly->FullName)) { + return baseScriptAssembly; + } + } + catch (...) + { + } + + return nullptr; +} + +IPluginInstaller::EInstallResult executeScript(System::String^ script) { + + using namespace System; + using namespace System::CodeDom; + using namespace System::CodeDom::Compiler; + using namespace System::Collections::Generic; + + AppDomain^ currentDomain = AppDomain::CurrentDomain; + currentDomain->AssemblyResolve += gcnew ResolveEventHandler(currentDomain_AssemblyResolve); + + // From Nexus-Mods/fomod-installer: + Dictionary^ dicOptions = gcnew Dictionary(10); + dicOptions->Add("CompilerVersion", "v4.0"); + + // List of assemblies (including BaseScript) - From Nexus-Mods/fomod-installer: + array^ referenceAssemblies = { + "System.dll", + "System.Runtime.dll", + "System.Drawing.dll", + "System.Windows.Forms.dll", + "System.Xml.dll", + System::Reflection::Assembly::GetAssembly(BaseScript::typeid)->Location + }; + + CompilerParameters^ cp = gcnew CompilerParameters(referenceAssemblies); + cp->GenerateExecutable = false; + cp->IncludeDebugInformation = false; + cp->GenerateInMemory = true; + cp->TreatWarningsAsErrors = false; + CodeDomProvider^ provider = CodeDomProvider::CreateProvider("CSharp", dicOptions); + + // Compile the script + auto result = provider->CompileAssemblyFromSource(cp, script); + + int errorCount = 0; + for each (CompilerError ^ error in result->Errors) { + if (error->IsWarning) { + log::warn("C# [{}]: {}", error->Line, CSharp::to_string(error->ErrorText)); + } + else { + log::error("C# [{}]: {}", error->Line, CSharp::to_string(error->ErrorText)); + ++errorCount; + } + } + + if (errorCount > 0) { + return IPluginInstaller::EInstallResult::RESULT_FAILED; + } + + // Execute the script: + try { + auto scriptClass = result->CompiledAssembly->GetType("Script"); + BaseScript^ scriptObject = (BaseScript^)System::Activator::CreateInstance(scriptClass); + auto onActivateMethod = scriptObject->GetType()->GetMethod("OnActivate"); + + auto success = (bool)(onActivateMethod->IsStatic ? onActivateMethod->Invoke(nullptr, nullptr) : onActivateMethod->Invoke(scriptObject, nullptr)); + return success ? IPluginInstaller::EInstallResult::RESULT_SUCCESS : IPluginInstaller::EInstallResult::RESULT_CANCELED; + } + catch (System::Exception^ ex) { + log::error("C# ({}): {}\n{}", CSharp::to_string(ex->GetType()->FullName), CSharp::to_string(ex->Message), CSharp::to_string(ex->StackTrace)); + System::Exception^ innerEx = ex->InnerException; + if (innerEx) { + log::error("C# ({}): {}\n{}", CSharp::to_string(innerEx->GetType()->FullName), CSharp::to_string(innerEx->Message), CSharp::to_string(innerEx->StackTrace)); + } + return IPluginInstaller::EInstallResult::RESULT_FAILED; + } + +} + +namespace CSharp { + + IPluginInstaller::EInstallResult executeCSharpScript(QString scriptPath, std::shared_ptr& tree) { + + using namespace System; + using namespace System::IO; + using namespace System::Text::RegularExpressions; + + // Note: Using C# stuff here to mimicate NMM since there are some encoding issues, and + // some regex do not work in C++: + array^ scriptBytes = File::ReadAllBytes(from_string(scriptPath.toStdWString())); + + // Read the script (using C# to "auto-detect" encoding in a C# way): + String^ script; + { + auto memoryStream = gcnew MemoryStream(scriptBytes); + auto reader = gcnew StreamReader(memoryStream, true); + + script = reader->ReadToEnd(); + + reader->Close(); + memoryStream->Close(); + + delete reader; + delete memoryStream; + } + + + Regex^ regScriptClass = gcnew Regex(R"re((class\s+Script\s*:.*?)(\S*BaseScript))re"); + Regex^ regFommUsing = gcnew Regex(R"re(\s*using\s*fomm.Scripting\s*;)re"); + + String^ strBaseScriptClassName = regScriptClass->Match(script)->Groups[2]->ToString(); + Regex^ regOtherScriptClasses = gcnew Regex(String::Format(R"re((class\s+\S+\s*:.*?)(?Replace(strCode, "$1BaseScript"); + strCode = regOtherScriptClasses->Replace(strCode, "$1BaseScript"); + strCode = regFommUsing->Replace(strCode, ""); + + auto result = executeScript(strCode); + + if (result != IPluginInstaller::EInstallResult::RESULT_SUCCESS) { + return result; + } + + return postInstall(tree); + } + +} diff --git a/libs/installer_fomod_csharp/src/csharp_interface.h b/libs/installer_fomod_csharp/src/csharp_interface.h new file mode 100644 index 0000000..4f22c20 --- /dev/null +++ b/libs/installer_fomod_csharp/src/csharp_interface.h @@ -0,0 +1,40 @@ +#ifndef CSHARP_INTERFACE_H +#define CSHARP_INTERFACE_H + +#include + +#include +#include + +namespace CSharp +{ + +void init(MOBase::IOrganizer* moInfo); + +/** + * @brief Initialize the C# interface before starting an installation. + * + * @param installer The FOMOD C# installer. + * @param manager The installation manager from the installer. + * @param parentWidget The parent widget from the installer. + * @param tree The archive tree. + * @param extractedEntries A map from extracted entries to their extracted path. + */ +void beforeInstall( + MOBase::IPlugin const* installer, MOBase::IInstallationManager* manager, + QWidget* parentWidget, std::shared_ptr tree, + std::map, QString> extractedEntries); + +/** + * @brief Clear the C# interface after an installation. + * + * @param scriptPath Path to the script to execute. + * @param tree Reference where the final tree will be stored (in case of success. + * + * @return the installation result after performing post-installation. + */ +MOBase::IPluginInstaller::EInstallResult +executeCSharpScript(QString scriptPath, std::shared_ptr& tree); + +} // namespace CSharp +#endif diff --git a/libs/installer_fomod_csharp/src/csharp_utils.h b/libs/installer_fomod_csharp/src/csharp_utils.h new file mode 100644 index 0000000..5ede874 --- /dev/null +++ b/libs/installer_fomod_csharp/src/csharp_utils.h @@ -0,0 +1,41 @@ +// clang-format off + +#ifndef CSHARP_UTILS_H +#define CSHARP_UTILS_H + +#include +#include + +#include + +#include + +#using + +namespace CSharp { + + /** + * Handy functions. + */ + inline std::string to_string(System::String^ value) { + return msclr::interop::marshal_as(value); + } + inline std::wstring to_wstring(System::String^ value) { + return msclr::interop::marshal_as(value); + } + inline QString to_qstring(System::String^ value) { + msclr::interop::marshal_context ctx; + return QString::fromWCharArray(ctx.marshal_as(value)); + } + + template + inline System::String^ from_string(Str const& string) { + return msclr::interop::marshal_as(string); + } + inline System::String^ from_string(QString const& string) { + return msclr::interop::marshal_as(string.toStdWString().c_str()); + } + +} + +#endif diff --git a/libs/installer_fomod_csharp/src/installer_fomod_csharp.cpp b/libs/installer_fomod_csharp/src/installer_fomod_csharp.cpp new file mode 100644 index 0000000..02d3a3e --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_csharp.cpp @@ -0,0 +1,193 @@ +/* +Copyright (C) 2020 Holt59. All rights reserved. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include + +#include "csharp_interface.h" +#include "installer_fomod_csharp.h" +#include "installer_fomod_predialog.h" +#include "xml_info_reader.h" + +using namespace MOBase; + +bool InstallerFomodCSharp::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + CSharp::init(moInfo); + return true; +} + +std::shared_ptr +InstallerFomodCSharp::findFomodDirectory(std::shared_ptr tree) const +{ + auto entry = tree->find("fomod", FileTreeEntry::DIRECTORY); + + if (entry != nullptr) { + return entry->astree(); + } + + if (tree->empty()) { + return nullptr; + } + + // We need at least a directory: + if (!tree->at(0)->isDir()) { + return nullptr; + } + + // But not two: + if (tree->size() > 1 && tree->at(1)->isDir()) { + return nullptr; + } + + return findFomodDirectory(tree->at(0)->astree()); +} + +std::shared_ptr +InstallerFomodCSharp::findScriptFile(std::shared_ptr tree) const +{ + auto fomodDirectory = findFomodDirectory(tree); + + if (fomodDirectory == nullptr) { + return nullptr; + } + + for (auto e : *fomodDirectory) { + if (e->isFile() && e->suffix().compare("cs", Qt::CaseInsensitive) == 0) { + return e; + } + } + + return nullptr; +} + +std::shared_ptr +InstallerFomodCSharp::findInfoFile(std::shared_ptr tree) const +{ + auto fomodDirectory = findFomodDirectory(tree); + + if (fomodDirectory == nullptr) { + return nullptr; + } + + for (auto e : *fomodDirectory) { + if (e->isFile() && e->compare("info.xml") == 0) { + return e; + } + } + + return nullptr; +} + +bool InstallerFomodCSharp::isArchiveSupported( + std::shared_ptr tree) const +{ + return findScriptFile(tree) != nullptr; +} + +InstallerFomodCSharp::EInstallResult +InstallerFomodCSharp::install(MOBase::GuessedValue& modName, + std::shared_ptr& tree, + QString& version, int& modID) +{ + static std::set imageSuffixes{"png", "jpg", "jpeg", + "gif", "bmp"}; + + // Extract the script file: + auto scriptFile = findScriptFile(tree); + if (scriptFile == nullptr) { + return EInstallResult::RESULT_NOTATTEMPTED; + } + + // Check if there is a info.xml: + auto infoFile = findInfoFile(tree); + + // Set containing everything to extract except the script and the info file: + std::set> toExtractSet{scriptFile}; + + if (infoFile != nullptr) { + toExtractSet.insert(infoFile); + } + + // Extract all the images: + tree->walk([&](const QString&, auto entry) { + if (entry->isFile() && imageSuffixes.count(entry->suffix()) > 0) { + toExtractSet.insert(entry); + } + return IFileTree::WalkReturn::CONTINUE; + }); + + // Extract everything from the fomod/ folder: + auto fomodFolder = findFomodDirectory(tree); + fomodFolder->walk([&](const QString&, auto entry) { + if (entry->isFile()) { + toExtractSet.insert(entry); + } + return IFileTree::WalkReturn::CONTINUE; + }); + + // Convert to vector: + std::vector toExtract(std::begin(toExtractSet), std::end(toExtractSet)); + QStringList paths(manager()->extractFiles(toExtract)); + + // If user cancelled: + if (toExtract.size() != static_cast(paths.size())) { + return EInstallResult::RESULT_CANCELED; + } + + // Create a map from entry to file path: + std::map, QString> entryToPath; + for (std::size_t i = 0; i < toExtract.size(); ++i) { + entryToPath[toExtract[i]] = paths[i]; + } + + if (infoFile != nullptr) { + QFile file(entryToPath[infoFile]); + if (file.open(QIODevice::ReadOnly)) { + auto info = FomodInfoReader::readXml(file, &FomodInfoReader::parseInfo); + if (!std::get<0>(info).isEmpty()) { + modName.update(std::get<0>(info), GUESS_META); + } + if (std::get<1>(info) != -1) { + modID = std::get<1>(info); + } + if (!std::get<2>(info).isEmpty()) { + version = std::get<2>(info); + } + } + } + + // Show the dialog: + InstallerFomodPredialog dialog(modName, parentWidget()); + if (dialog.exec() != QDialog::Accepted) { + if (dialog.manualRequested()) { + modName.update(dialog.getName(), GUESS_USER); + return EInstallResult::RESULT_MANUALREQUESTED; + } else { + return EInstallResult::RESULT_CANCELED; + } + } + modName.update(dialog.getName(), GUESS_USER); + + // Run the C# script: + const QString scriptPath = entryToPath[scriptFile]; + CSharp::beforeInstall( + this, manager(), parentWidget(), + std::const_pointer_cast(scriptFile->parent()->parent()), + std::move(entryToPath)); + return CSharp::executeCSharpScript(scriptPath, tree); +} diff --git a/libs/installer_fomod_csharp/src/installer_fomod_csharp.h b/libs/installer_fomod_csharp/src/installer_fomod_csharp.h new file mode 100644 index 0000000..530db37 --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_csharp.h @@ -0,0 +1,84 @@ +/* +Copyright (C) 2020 Holt59. All rights reserved. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef INSTALLER_FOMOD_CSHARP_H +#define INSTALLER_FOMOD_CSHARP_H + +#include + +class InstallerFomodCSharp : public MOBase::IPluginInstallerSimple +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) + Q_PLUGIN_METADATA(IID "org.holt59.InstallerFomodCSharp") + +public: + InstallerFomodCSharp() {} + + virtual bool init(MOBase::IOrganizer* moInfo) override; + + virtual QString name() const override { return "Fomod Installer C#"; } + + virtual QString localizedName() const override { return tr("Fomod Installer C#"); } + + virtual QString author() const override { return "Holt59"; } + + virtual QString description() const override + { + return tr("Installer for C# based FOMOD archives."); + } + + virtual MOBase::VersionInfo version() const override + { + return MOBase::VersionInfo(1, 0, 0, MOBase::VersionInfo::RELEASE_BETA); + } + + virtual QList settings() const override + { + return { + MOBase::PluginSetting("enabled", "check to enable this plugin", QVariant(true)), + MOBase::PluginSetting("prefer", "prefer this over the NCC based plugin", + QVariant(true))}; + } + + virtual unsigned int priority() const override + { + // It's the same priority as the FOMOD installer but those should never conflict: + return m_MOInfo->pluginSetting(name(), "prefer").toBool() ? 110 : 90; + } + + virtual bool isManualInstaller() const override { return false; } + + virtual bool + isArchiveSupported(std::shared_ptr tree) const override; + + virtual EInstallResult install(MOBase::GuessedValue& modName, + std::shared_ptr& tree, + QString& version, int& modID) override; + +private: + MOBase::IOrganizer* m_MOInfo; + + std::shared_ptr + findFomodDirectory(std::shared_ptr tree) const; + std::shared_ptr + findScriptFile(std::shared_ptr tree) const; + std::shared_ptr + findInfoFile(std::shared_ptr tree) const; +}; + +#endif diff --git a/libs/installer_fomod_csharp/src/installer_fomod_csharp_en.ts b/libs/installer_fomod_csharp/src/installer_fomod_csharp_en.ts new file mode 100644 index 0000000..7c13b6c --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_csharp_en.ts @@ -0,0 +1,125 @@ + + + + + FomodCSharpPostDialog + + + Settings modification required + + + + + The installer needs to edit the following settings. You can either apply them, discard them or move them to the mod installation folder (under INI Tweaks). + + + + + Apply the settings to the INI files corresponding to the current profile. + + + + + Apply + + + + + Discard the settings. + + + + + Discard + + + + + Create files under "INI Tweaks" in the mod folder with these settings. + + + + + INI Tweaks + + + + + Cancel the installation. + + + + + Cancel + + + + + FomodCSharpPredialog + + + FOMOD C# Installer + + + + + Name + + + + + + Opens a Dialog that allows custom modifications. + + + + + Manual + + + + + Start + + + + + Cancel + + + + + FomodInfoReader + + + Failed to parse %1. See console for details. + + + + + InstallerFomodCSharp + + + Fomod Installer C# + + + + + Installer for C# based FOMOD archives. + + + + + QObject + + + Choose any: + + + + + Choose one: + + + + diff --git a/libs/installer_fomod_csharp/src/installer_fomod_csharp_postdialog.ui b/libs/installer_fomod_csharp/src/installer_fomod_csharp_postdialog.ui new file mode 100644 index 0000000..89a6106 --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_csharp_postdialog.ui @@ -0,0 +1,99 @@ + + + FomodCSharpPostDialog + + + + 0 + 0 + 589 + 399 + + + + Settings modification required + + + + + + The installer needs to edit the following settings. You can either apply them, discard them or move them to the mod installation folder (under INI Tweaks). + + + true + + + + + + + + + -1 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Apply the settings to the INI files corresponding to the current profile. + + + Apply + + + + + + + Discard the settings. + + + Discard + + + + + + + Create files under "INI Tweaks" in the mod folder with these settings. + + + INI Tweaks + + + + + + + Cancel the installation. + + + Cancel + + + + + + + + + + diff --git a/libs/installer_fomod_csharp/src/installer_fomod_csharp_predialog.ui b/libs/installer_fomod_csharp/src/installer_fomod_csharp_predialog.ui new file mode 100644 index 0000000..0aa08c3 --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_csharp_predialog.ui @@ -0,0 +1,89 @@ + + + FomodCSharpPredialog + + + + 0 + 0 + 400 + 83 + + + + FOMOD C# Installer + + + + + + + + Name + + + + + + + true + + + + + + + + + + + Opens a Dialog that allows custom modifications. + + + Opens a Dialog that allows custom modifications. + + + Manual + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 20 + 20 + + + + + + + + Start + + + true + + + + + + + Cancel + + + + + + + + + + diff --git a/libs/installer_fomod_csharp/src/installer_fomod_postdialog.h b/libs/installer_fomod_csharp/src/installer_fomod_postdialog.h new file mode 100644 index 0000000..9804610 --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_postdialog.h @@ -0,0 +1,87 @@ +#ifndef INSTALLER_FOMOD_POSTDIALOG_H +#define INSTALLER_FOMOD_POSTDIALOG_H + +#include "ui_installer_fomod_csharp_postdialog.h" + +#include + +#include "psettings.h" + +/** + * @brief Dialog for the installation of a simple archive + * a simple archive is one that doesn't require any manual changes to work correctly + **/ +class InstallerFomodPostDialog : public QDialog +{ + Q_OBJECT + +public: + enum class Result + { + APPLY, + DISCARD, + MOVE, + }; + + /** + * @brief constructor + * + * @param preset suggested name for the mod + * @param parent parent widget + **/ + explicit InstallerFomodPostDialog(QWidget* parent = 0) + : QDialog(parent), ui(new Ui::FomodCSharpPostDialog) + { + ui->setupUi(this); + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + } + + ~InstallerFomodPostDialog() {} + + /** + * @return the result of this dialog if the user did not cancel. + */ + Result result() const { return m_Result; } + + /** + * + */ + void setIniSettings(std::map const& settings) + { + for (auto p : settings) { + QTextEdit* widget = new QTextEdit(this); + widget->append(p.second.toString()); + widget->setReadOnly(true); + ui->tabWidget->addTab(widget, p.first); + } + } + +private slots: + + void on_discardBtn_clicked() + { + m_Result = Result::DISCARD; + this->accept(); + } + + void on_applyBtn_clicked() + { + m_Result = Result::APPLY; + this->accept(); + } + + void on_moveBtn_clicked() + { + m_Result = Result::MOVE; + this->accept(); + } + + void on_cancelBtn_clicked() { this->reject(); } + +private: + std::unique_ptr ui; + + Result m_Result{Result::APPLY}; +}; + +#endif diff --git a/libs/installer_fomod_csharp/src/installer_fomod_predialog.h b/libs/installer_fomod_csharp/src/installer_fomod_predialog.h new file mode 100644 index 0000000..5c57fdf --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_predialog.h @@ -0,0 +1,72 @@ +#ifndef INSTALLER_FOMOD_PREDIALOG_H +#define INSTALLER_FOMOD_PREDIALOG_H + +#include "ui_installer_fomod_csharp_predialog.h" + +#include + +#include + +/** + * @brief Dialog for the installation of a simple archive + * a simple archive is one that doesn't require any manual changes to work correctly + **/ +class InstallerFomodPredialog : public QDialog +{ + Q_OBJECT + +public: + /** + * @brief constructor + * + * @param preset suggested name for the mod + * @param parent parent widget + **/ + explicit InstallerFomodPredialog(const MOBase::GuessedValue& preset, + QWidget* parent = 0) + : QDialog(parent), ui(new Ui::FomodCSharpPredialog), m_Manual(false) + { + + ui->setupUi(this); + setWindowTitle(preset + " - " + windowTitle()); + + for (auto iter = preset.variants().begin(); iter != preset.variants().end(); + ++iter) { + ui->nameCombo->addItem(*iter); + } + + ui->nameCombo->setCurrentIndex(ui->nameCombo->findText(preset)); + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + ui->nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); + } + + ~InstallerFomodPredialog() {} + + /** + * @return true if the user requested a manual installation. + **/ + bool manualRequested() const { return m_Manual; } + + /** + * @return the (user-modified) mod name + **/ + QString getName() const { return ui->nameCombo->currentText(); } + +private slots: + + void on_okBtn_clicked() { this->accept(); } + + void on_cancelBtn_clicked() { this->reject(); } + + void on_manualBtn_clicked() + { + m_Manual = true; + this->reject(); + } + +private: + std::unique_ptr ui; + bool m_Manual; +}; + +#endif diff --git a/libs/installer_fomod_csharp/src/psettings.h b/libs/installer_fomod_csharp/src/psettings.h new file mode 100644 index 0000000..649c7da --- /dev/null +++ b/libs/installer_fomod_csharp/src/psettings.h @@ -0,0 +1,108 @@ +#ifndef PSETTINGS_H +#define PSETTINGS_H + +#include +#include +#include + +/** + * This is a small class that can be used to store INI settings in memory since + * QSettings is a pain to use without an actual file. + * + * It is a much simpler structure since it stores everything as string. + */ +struct PSettings +{ + + /** + * + */ + PSettings() = default; + +public: // Value read/write. + /** + * @brief Set the value of the given section/key. + * + * @param section The section of the value. + * @param key The key of the value. + * @param value The value to set. + */ + void setValue(QString section, QString key, QString value) + { + m_Values[std::make_pair(section, key)] = value; + } + + /** + * @brief Return the value at the given section/key. + * + * @param section The section of the value. + * @param key The key of the value. + * + * @return the corresponding value, or an empty string if the section/key does not + * exist. + */ + QString value(QString section, QString key) const + { + auto it = m_Values.find(std::make_pair(section, key)); + return it == m_Values.end() ? QString() : it->second; + } + + /** + * @brief Check if the given section/key exists in these settings. + * + * @param section The section of the value. + * @param key The key of the value. + * + * @return true if the section/key exist. + */ + bool hasValue(QString section, QString key) const + { + return m_Values.find(std::make_pair(section, key)) != m_Values.end(); + } + +public: // Output: + /** + * @brief Convert this PSettings to a string. + * + * @return a string representing the content of a valid INI file corresponding + * to this PSettings. + */ + QString toString() const + { + QString result = ""; + QString cSection; + for (auto& p : m_Values) { + if (cSection != p.first.first) { + if (!cSection.isEmpty()) { + result += '\n'; + } + cSection = p.first.first; + result += "[" + cSection + "]\n"; + } + result += p.first.second + "=" + p.second + "\n"; + } + return result; + } + + /** + * @brief Update the given QSettings with all the value in this. + * + * @param settings The settings to update. + */ + void update(QSettings& settings) const + { + for (auto& p : m_Values) { + if (p.first.first == "General") { + settings.setValue(p.first.second, p.second); + } else { + settings.setValue(p.first.first + "/" + p.first.second, p.second); + } + } + } + +private: + // Map from to value: + std::map, QString> m_Values; +}; + +#endif // !PSETTINGS_H diff --git a/libs/installer_fomod_csharp/src/xml_info_reader.h b/libs/installer_fomod_csharp/src/xml_info_reader.h new file mode 100644 index 0000000..91d6d9a --- /dev/null +++ b/libs/installer_fomod_csharp/src/xml_info_reader.h @@ -0,0 +1,140 @@ +#ifndef XML_INFO_READER_H +#define XML_INFO_READER_H + +#include +#include +#include +#include +#include +#include + +#include +#include + +// This is from installer_fomod, but should probably not be duplicated here. + +struct FomodInfoReader : QObject +{ + + Q_OBJECT + +public: + struct XmlParseError : MOBase::Exception + { + XmlParseError(const QString& message) : MOBase::Exception(message) {} + }; + + static QByteArray skipXmlHeader(QIODevice& file) + { + static const unsigned char UTF16LE_BOM[] = {0xFF, 0xFE}; + static const unsigned char UTF16BE_BOM[] = {0xFE, 0xFF}; + static const unsigned char UTF8_BOM[] = {0xEF, 0xBB, 0xBF}; + static const unsigned char UTF16LE[] = {0x3C, 0x00, 0x3F, 0x00}; + static const unsigned char UTF16BE[] = {0x00, 0x3C, 0x00, 0x3F}; + static const unsigned char UTF8[] = {0x3C, 0x3F, 0x78, 0x6D}; + + file.seek(0); + QByteArray rawBytes = file.read(4); + QTextStream stream(&file); + int bom = 0; + if (rawBytes.startsWith((const char*)UTF16LE_BOM)) { + stream.setEncoding(QStringEncoder::Encoding::Utf16LE); + bom = 2; + } else if (rawBytes.startsWith((const char*)UTF16BE_BOM)) { + stream.setEncoding(QStringEncoder::Encoding::Utf16BE); + bom = 2; + } else if (rawBytes.startsWith((const char*)UTF8_BOM)) { + stream.setEncoding(QStringEncoder::Encoding::Utf8); + bom = 3; + } else if (rawBytes.startsWith(QByteArray((const char*)UTF16LE, 4))) { + stream.setEncoding(QStringEncoder::Encoding::Utf16LE); + } else if (rawBytes.startsWith(QByteArray((const char*)UTF16BE, 4))) { + stream.setEncoding(QStringEncoder::Encoding::Utf16BE); + } else if (rawBytes.startsWith(QByteArray((const char*)UTF8, 4))) { + stream.setEncoding(QStringEncoder::Encoding::Utf8); + } // otherwise maybe the textstream knows the encoding? + + stream.seek(bom); + QString header = stream.readLine(); + if (!header.startsWith(" + static auto readXml(QFile& file, Fn&& fn) + { + // List of encodings to try: + static const std::vector encodings{ + QStringConverter::Utf16, QStringConverter::Utf8, QStringConverter::Latin1}; + + std::string errorMessage; + try { + QXmlStreamReader reader(&file); + return fn(reader); + } catch (const XmlParseError& e) { + MOBase::log::warn( + "The {} in this file is incorrectly encoded ({}). Applying heuristics...", + file.fileName(), e.what()); + } + + // nmm's xml parser is less strict than the one from qt and allows files with + // wrong encoding in the header. Being strict here would be bad user experience + // this works around bad headers. + QByteArray headerlessData = skipXmlHeader(file); + + // try parsing the file with several encodings to support broken files + for (auto encoding : encodings) { + MOBase::log::debug("Trying encoding {} for {}... ", + QStringConverter::nameForEncoding(encoding), file.fileName()); + try { + QStringEncoder encoder(encoding); + QXmlStreamReader reader( + encoder.encode(QString("") + .arg(encoder.name())) + + headerlessData); + MOBase::log::debug("Interpreting {} as {}.", file.fileName(), encoder.name()); + return fn(reader); + } catch (const XmlParseError& e) { + MOBase::log::debug("Not {}: {}.", QStringConverter::nameForEncoding(encoding), + e.what()); + } + } + + throw XmlParseError( + tr("Failed to parse %1. See console for details.").arg(file.fileName())); + } + + static std::tuple parseInfo(QXmlStreamReader& reader) + { + std::tuple info{"", -1, ""}; + while (!reader.atEnd()) { + switch (reader.readNext()) { + case QXmlStreamReader::StartElement: { + if (reader.name().toString() == "Name") { + std::get<0>(info) = reader.readElementText(); + } else if (reader.name().toString() == "Author") { + } else if (reader.name().toString() == "Version") { + std::get<2>(info) = reader.readElementText(); + } else if (reader.name().toString() == "Id") { + std::get<1>(info) = reader.readElementText().toInt(); + } else if (reader.name().toString() == "Website") { + } + } break; + default: { + } break; + } + } + if (reader.hasError()) { + throw XmlParseError( + QString("%1 in line %2").arg(reader.errorString()).arg(reader.lineNumber())); + } + return info; + } +}; + +#endif diff --git a/libs/installer_fomod_csharp/vcpkg.json b/libs/installer_fomod_csharp/vcpkg.json new file mode 100644 index 0000000..28051ad --- /dev/null +++ b/libs/installer_fomod_csharp/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673" + } + } +} diff --git a/libs/installer_manual/.clang-format b/libs/installer_manual/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/installer_manual/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/installer_manual/.git-blame-ignore-revs b/libs/installer_manual/.git-blame-ignore-revs new file mode 100644 index 0000000..8fb9bf6 --- /dev/null +++ b/libs/installer_manual/.git-blame-ignore-revs @@ -0,0 +1 @@ +1d3cd89fc817c47142dc3f6370a37801b4ea3f90 diff --git a/libs/installer_manual/.gitattributes b/libs/installer_manual/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/installer_manual/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/installer_manual/.github/workflows/build.yml b/libs/installer_manual/.github/workflows/build.yml new file mode 100644 index 0000000..66517c2 --- /dev/null +++ b/libs/installer_manual/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build Installer Manual + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Installer Manual + id: build-installer-manual + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase diff --git a/libs/installer_manual/.github/workflows/linting.yml b/libs/installer_manual/.github/workflows/linting.yml new file mode 100644 index 0000000..1b58829 --- /dev/null +++ b/libs/installer_manual/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Installer Manual Plugin + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/installer_manual/.gitignore b/libs/installer_manual/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/installer_manual/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/installer_manual/.pre-commit-config.yaml b/libs/installer_manual/.pre-commit-config.yaml new file mode 100644 index 0000000..3103a1f --- /dev/null +++ b/libs/installer_manual/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v19.1.5 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/installer_manual/CMakeLists.txt b/libs/installer_manual/CMakeLists.txt new file mode 100644 index 0000000..ce1b2f1 --- /dev/null +++ b/libs/installer_manual/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(installer_manual) + +add_subdirectory(src) diff --git a/libs/installer_manual/CMakePresets.json b/libs/installer_manual/CMakePresets.json new file mode 100644 index 0000000..5a63c77 --- /dev/null +++ b/libs/installer_manual/CMakePresets.json @@ -0,0 +1,67 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/installer_manual/src/CMakeLists.txt b/libs/installer_manual/src/CMakeLists.txt new file mode 100644 index 0000000..2254977 --- /dev/null +++ b/libs/installer_manual/src/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB installer_manual_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(installer_manual SHARED ${installer_manual_SOURCES}) +mo2_configure_plugin(installer_manual NO_SOURCES WARNINGS 4) +target_include_directories(installer_manual PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) +target_link_libraries(installer_manual PRIVATE mo2::uibase) +mo2_install_plugin(installer_manual) diff --git a/libs/installer_manual/src/archivetree.cpp b/libs/installer_manual/src/archivetree.cpp new file mode 100644 index 0000000..ad9319e --- /dev/null +++ b/libs/installer_manual/src/archivetree.cpp @@ -0,0 +1,486 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "archivetree.h" + +#include +#include +#include + +#include +#include +#include + +using namespace MOBase; + +// Implementation details for the ArchiveTree widget: +// +// The ArchiveTreeWidget presents to the user the underlying IFileTree, but in order +// to increase performance, the tree is populated dynamically when required. Populating +// the tree is currently required: +// 1) when a branch of the tree widget is expanded, +// 2) when an item is moved to a tree, +// 3) when a directory is created, +// 4) when a directory is "set as data root". +// +// Case 1 is handled automatically in the setExpanded method of ArchiveTreeWidget. Cases +// 2 and 3 could be dealt with differently, but populating the tree before inserting an +// item makes everything else easier (not that populating the widget is different from +// populating the IFileTree which is done automatically). Case 4 is handled manually in +// setDataRoot. +// +// Another specificity of the implementation is the treeCheckStateChanged() signal +// emitted by the ArchiveTreeWidget. This signal is used to avoid having to connect to +// the itemChanged() signal or overriding the dataChanged() method which are called much +// more often than those. The treeCheckStateChanged() signal is send only for the item +// that has actually been changed by the user. While the interface is automatically +// updated by Qt, we need to update the underlying tree manually. This is done by doing +// the following things: +// 1) When an item is unchecked: +// - We detach the corresponding entry from its parent, and recursively detach the +// empty +// parents (or the ones that become empty). +// - If the entry is a directory and the item has been populated, we recursively +// detach +// all the child entries for all the child items that have been populated (no +// need to do it for non-populated items)> +// 2) When an item is checked, we do the same process but we re-attach parents and +// re-insert +// children. +// +// Detaching or re-attaching parents is also done when a directory is created (if the +// directory is created in an empty directory, we need to re-attach), or when an item is +// moved (if the directory the item comes from is now empty or if the target directory +// was empty). +// + +ArchiveTreeWidgetItem::ArchiveTreeWidgetItem(QString dataName) + : QTreeWidgetItem(QStringList(dataName)), m_Entry(nullptr) +{ + setFlags(flags() & ~Qt::ItemIsUserCheckable); + setExpanded(true); + m_Populated = true; +} + +ArchiveTreeWidgetItem::ArchiveTreeWidgetItem( + std::shared_ptr entry) + : QTreeWidgetItem(QStringList(entry->name())), m_Entry(entry) +{ + if (entry->isDir()) { + setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); + setFlags(flags() | Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate); + } else { + setFlags(flags() | Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren); + } + setCheckState(0, Qt::Checked); + setToolTip(0, entry->path()); +} + +void ArchiveTreeWidgetItem::setData(int column, int role, const QVariant& value) +{ + ArchiveTreeWidget* tree = static_cast(treeWidget()); + if (tree != nullptr && tree->m_Emitter == nullptr) { + tree->m_Emitter = this; + } + QTreeWidgetItem::setData(column, role, value); + if (tree != nullptr && tree->m_Emitter == this) { + tree->m_Emitter = nullptr; + if (role == Qt::CheckStateRole) { + tree->onTreeCheckStateChanged(this); + } + } +} + +void ArchiveTreeWidgetItem::populate(bool force) +{ + + // Only populates once: + if (isPopulated() && !force) { + return; + } + + // Should never happen: + if (entry()->isFile()) { + return; + } + + // We go in reverse of the tree because we want to insert the original + // entries at the beginning (the item can only contains children if a + // directory has been created under it or if entries has been moved under + // it): + for (auto& entry : *entry()->astree()) { + auto newItem = new ArchiveTreeWidgetItem(entry); + newItem->setCheckState(0, flags().testFlag(Qt::ItemIsUserCheckable) ? checkState(0) + : Qt::Checked); + addChild(newItem); + } + + // If the item is unchecked, we need to clear it because it has not been cleared + // before: + if (flags().testFlag(Qt::ItemIsUserCheckable) && checkState(0) == Qt::Unchecked) { + entry()->astree()->clear(); + } + + m_Populated = true; +} + +ArchiveTreeWidget::ArchiveTreeWidget(QWidget* parent) : QTreeWidget(parent) +{ + setAutoExpandDelay(1000); + setDragDropOverwriteMode(true); + connect(this, &ArchiveTreeWidget::itemExpanded, this, + &ArchiveTreeWidget::populateItem); +} + +void ArchiveTreeWidget::setup(QString dataFolderName) +{ + m_ViewRoot = new ArchiveTreeWidgetItem("<" + dataFolderName + ">"); + m_DataRoot = nullptr; + addTopLevelItem(m_ViewRoot); +} + +void ArchiveTreeWidget::populateItem(QTreeWidgetItem* item) +{ + static_cast(item)->populate(); +} + +void ArchiveTreeWidget::setDataRoot(ArchiveTreeWidgetItem* const root) +{ + if (root != m_DataRoot) { + if (m_DataRoot != nullptr) { + m_DataRoot->addChildren(m_ViewRoot->takeChildren()); + } + + // Force populate: + root->populate(); + + m_DataRoot = root; + m_ViewRoot->setEntry(m_DataRoot->entry()); + m_ViewRoot->addChildren(m_DataRoot->takeChildren()); + m_ViewRoot->setExpanded(true); + } + + emit treeChanged(); +} + +void ArchiveTreeWidget::detachParents(ArchiveTreeWidgetItem* item) +{ + auto entry = item->entry(); + auto parent = entry->parent(); + entry->detach(); + while (parent != nullptr && parent->empty()) { + auto tmp = parent->parent(); + parent->detach(); + parent = tmp; + } +} + +void ArchiveTreeWidget::attachParents(ArchiveTreeWidgetItem* item) +{ + while (item->parent() != nullptr) { + auto parent = static_cast(item->parent()); + auto parentEntry = parent->entry(); + if (parentEntry != nullptr) { + parentEntry->astree()->insert(item->entry()); + } + item = parent; + } +} + +void ArchiveTreeWidget::recursiveInsert(ArchiveTreeWidgetItem* item) +{ + if (item->isPopulated()) { + auto tree = item->entry()->astree(); + for (int i = 0; i < item->childCount(); ++i) { + auto child = static_cast(item->child(i)); + tree->insert(child->entry()); + if (child->entry()->isDir()) { + recursiveInsert(child); + } + } + } +} + +void ArchiveTreeWidget::recursiveDetach(ArchiveTreeWidgetItem* item) +{ + if (item->isPopulated()) { + for (int i = 0; i < item->childCount(); ++i) { + auto child = static_cast(item->child(i)); + if (child->entry()->isDir()) { + recursiveDetach(child); + } + } + item->entry()->astree()->clear(); + } +} + +ArchiveTreeWidgetItem* ArchiveTreeWidget::addDirectory(ArchiveTreeWidgetItem* item, + QString name) +{ + auto tree = item->entry()->astree(); + auto* newItem = new ArchiveTreeWidgetItem(tree->addDirectory(name)); + + // find the insert position + auto it = std::find_if(tree->begin(), tree->end(), [name](auto&& entry) { + return entry->compare(name) == 0; + }); + int index = it - tree->begin(); + MOBase::log::debug("insert at: {}", index); + item->insertChild(index, newItem); + + newItem->setCheckState(0, Qt::Checked); + attachParents(item); + emit treeChanged(); + + return newItem; +} + +void ArchiveTreeWidget::moveItem(ArchiveTreeWidgetItem* source, + ArchiveTreeWidgetItem* target) +{ + // just insert the source in the target. + auto tree = target->entry()->astree(); + + detachParents(source); + + // check if an entry exists with the same name, we check + // in the tree widget to find unchecked items + for (int i = 0; i < target->childCount(); ++i) { + auto* child = target->child(i); + if (child->entry()->compare(source->entry()->name()) == 0) { + // remove existing file and force check existing directory + if (child->entry()->isFile()) { + target->removeChild(child); + } else { + child->setCheckState(0, Qt::Checked); + } + break; + } + } + + tree->insert(source->entry(), IFileTree::InsertPolicy::MERGE); + + attachParents(target); + + emit treeChanged(); +} + +void ArchiveTreeWidget::onTreeCheckStateChanged(ArchiveTreeWidgetItem* item) +{ + + auto entry = item->entry(); + + // If the entry is a directory, we need to either detach or re-attach all the + // children. It is not possible to only detach the directory because if the + // user uncheck a directory and then check a file under it, the other files would + // still be attached. + // + // The two recursive methods only go down to the expanded (based on isPopulated() + // tree, for two reasons: + // 1. If a tree item has not been populated, then detaching an entry from its parent + // will + // delete it since there would be no remaining shared pointers. + // 2. If the tree has not been populated yet, all the entries under it are still + // attached, + // so there is no need to process them differently. Detaching a non-expanded item + // can be done by simply detaching the tree, no need to detach all the children. + if (entry->isDir()) { + if (item->checkState(0) == Qt::Checked && item->isPopulated()) { + recursiveInsert(item); + } else if (item->checkState(0) == Qt::Unchecked && item->isPopulated()) { + recursiveDetach(item); + } + } + + // Unchecked: we go up the parent chain removing all trees that are now empty: + if (item->checkState(0) == Qt::Unchecked) { + detachParents(item); + } + // Otherwize, we need to-reattach the parent: + else { + attachParents(item); + } + + emit treeChanged(); +} + +bool ArchiveTreeWidget::testMovePossible(ArchiveTreeWidgetItem* source, + ArchiveTreeWidgetItem* target) +{ + if (target == nullptr || source == nullptr) { + return false; + } + + if (target->flags().testFlag(Qt::ItemNeverHasChildren)) { + return false; + } + + if (source == target || source->parent() == target) { + return false; + } + + return true; +} + +void ArchiveTreeWidget::dragEnterEvent(QDragEnterEvent* event) +{ + QTreeWidgetItem* source = this->currentItem(); + if ((source == nullptr) || (source->parent() == nullptr)) { + // can't change top level + event->ignore(); + return; + } else { + QTreeWidget::dragEnterEvent(event); + } +} + +void ArchiveTreeWidget::dragMoveEvent(QDragMoveEvent* event) +{ + if (!testMovePossible( + static_cast(currentItem()), + static_cast(itemAt(event->position().toPoint())))) { + event->ignore(); + } else { + QTreeWidget::dragMoveEvent(event); + } +} + +static bool isAncestor(const QTreeWidgetItem* ancestor, const QTreeWidgetItem* item) +{ + QTreeWidgetItem* iter = item->parent(); + while (iter != nullptr) { + if (iter == ancestor) { + return true; + } + iter = iter->parent(); + } + return false; +} + +void ArchiveTreeWidget::refreshItem(ArchiveTreeWidgetItem* item) +{ + if (!item->isPopulated() || item->flags().testFlag(Qt::ItemNeverHasChildren)) { + return; + } + + // at this point, all child items are checked for we only remember the ones + // that were expanded to re-expand them + std::map expanded; + while (item->childCount() > 0) { + auto* child = item->child(0); + expanded[child->entry()->name()] = child->isExpanded(); + item->removeChild(child); + } + + item->populate(true); + + for (int i = 0; i < item->childCount(); ++i) { + auto* child = item->child(i); + if (expanded[child->entry()->name()]) { + child->setExpanded(true); + } + } +} + +void ArchiveTreeWidget::dropEvent(QDropEvent* event) +{ + event->ignore(); + + // target widget (should be a directory) + auto* target = + static_cast(itemAt(event->position().toPoint())); + + // this should not really happen because it is prevent by dragMoveEvent + if (target->flags().testFlag(Qt::ItemNeverHasChildren)) { + + // this should really not happen, how should a file get to the top level? + if (target->parent() == nullptr) { + return; + } + + target = target->parent(); + } + + // populate target if required + target->populate(); + + auto sourceItems = this->selectedItems(); + + // check the selected items - we do not want to move only + // some items so we check everything first and then move + for (auto* source : sourceItems) { + + auto* aSource = static_cast(source); + + // do not allow element to be dropped into one of its + // own child + if (isAncestor(source, target)) { + event->accept(); + QMessageBox::warning(parentWidget(), tr("Cannot drop"), + tr("Cannot drop '%1' into one of its subfolder.") + .arg(aSource->entry()->name())); + return; + } + + auto sourceEntry = aSource->entry(); + auto targetEntry = target->entry()->astree()->find(sourceEntry->name()); + if (targetEntry && targetEntry->fileType() != sourceEntry->fileType()) { + event->accept(); + QMessageBox::warning(parentWidget(), tr("Cannot drop"), + targetEntry->isFile() + ? tr("A file '%1' already exists in folder '%2'.") + .arg(sourceEntry->name()) + .arg(target->entry()->name()) + : tr("A folder '%1' already exists in folder '%2'.") + .arg(sourceEntry->name()) + .arg(target->entry()->name())); + return; + } + } + + for (auto* source : sourceItems) { + + auto* aSource = static_cast(source); + + // this only check dropping an item on itself or dropping an item in + // its parent so it is ok, it just does not do anything + if (source->parent() == nullptr || !testMovePossible(aSource, target)) { + continue; + } + + // force expand item that are going to be merged + for (int i = 0; i < target->childCount(); ++i) { + auto* child = target->child(i); + if (child->entry()->compare(aSource->entry()->name()) == 0 && + !child->flags().testFlag(Qt::ItemNeverHasChildren)) { + child->setExpanded(true); + } + } + + // remove the source from its parent + source->parent()->removeChild(source); + + // actually perform the move on the underlying tree model + moveItem(aSource, target); + } + + // refresh the target item - this assumes that itemMoved is called synchronously + // and perform the FileTree changes + refreshItem(target); +} diff --git a/libs/installer_manual/src/archivetree.h b/libs/installer_manual/src/archivetree.h new file mode 100644 index 0000000..156966c --- /dev/null +++ b/libs/installer_manual/src/archivetree.h @@ -0,0 +1,178 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef ARCHIVETREE_H +#define ARCHIVETREE_H + +#include + +#include + +class ArchiveTreeWidget; + +// custom tree widget that holds a shared pointer to the file tree entry +// they represent +// +class ArchiveTreeWidgetItem : public QTreeWidgetItem +{ +public: + ArchiveTreeWidgetItem(QString dataName); + ArchiveTreeWidgetItem(std::shared_ptr entry); + +public: + // populate this tree widget item if it has not been populated yet + // or if force is true + // + void populate(bool force = false); + + // check if this item has already been populated + // + bool isPopulated() const { return m_Populated; } + + // replace the entry corresponding to this item + // + void setEntry(std::shared_ptr entry) { m_Entry = entry; } + + // retrieve the entry corresponding to this item + // + std::shared_ptr entry() const { return m_Entry; } + + // overriden method to avoid propagating dataChanged events + // + void setData(int column, int role, const QVariant& value) override; + + ArchiveTreeWidgetItem* parent() const + { + return static_cast(QTreeWidgetItem::parent()); + } + + ArchiveTreeWidgetItem* child(int index) const + { + return static_cast(QTreeWidgetItem::child(index)); + } + +protected: + std::shared_ptr m_Entry; + bool m_Populated = false; + + friend class ArchiveTreeWidget; +}; + +// Qt tree widget used to display the content of an archive in the manual installation +// dialog +class ArchiveTreeWidget : public QTreeWidget +{ + + Q_OBJECT + +public: + explicit ArchiveTreeWidget(QWidget* parent = 0); + void setup(QString dataFolderName); + +public: + // set the data root widget + // + void setDataRoot(ArchiveTreeWidgetItem* const root); + + // create a directory under the given tree item, without + // performing any check + // + ArchiveTreeWidgetItem* addDirectory(ArchiveTreeWidgetItem* treeItem, QString name); + + // return the root of the tree (the item corresponding to ) + // + ArchiveTreeWidgetItem* root() const { return m_ViewRoot; } + +signals: + + // emitted when the tree has been modified + // + void treeChanged(); + +public slots: + +protected: + // detach the entry of this item from its parent, and recursively detach + // all of its parent if they become + // + void detachParents(ArchiveTreeWidgetItem* item); + + // re-attach the entry of this item to its parent, and recursively attach + // all of its parent if they were empty (and thus detached) + // + void attachParents(ArchiveTreeWidgetItem* item); + + // recursively re-insert all the entries below the given item in their + // corresponding parents + // + // this method does not recurse in items that have not been populated yet + // + void recursiveInsert(ArchiveTreeWidgetItem* item); + + // recursively detach all the entries below the given item from their + // corresponding parents + // + // this method does not recurse in items that have not been populated yet + // + void recursiveDetach(ArchiveTreeWidgetItem* item); + + // slot that trigger the given item to be populated if it has not already + // been + // + void populateItem(QTreeWidgetItem* item); + + // move the source under the target + // + void moveItem(ArchiveTreeWidgetItem* source, ArchiveTreeWidgetItem* target); + + // called when the state of the item changed - unlike the standard QTreeWidget, + // this is only called for the actual item, not its parent/children + // + void onTreeCheckStateChanged(ArchiveTreeWidgetItem* item); + + void dragEnterEvent(QDragEnterEvent* event) override; + void dragMoveEvent(QDragMoveEvent* event) override; + void dropEvent(QDropEvent* event) override; + +private: + bool testMovePossible(ArchiveTreeWidgetItem* source, ArchiveTreeWidgetItem* target); + + // refresh the given item (after a drop) + // + void refreshItem(ArchiveTreeWidgetItem* item); + + // the widget item that emitted the dataChanged event + ArchiveTreeWidgetItem* m_Emitter = nullptr; + + // IMPORTANT: if you intend to work on this and understand this, read the detailed + // explanation at the beginning of the archivetree.cpp file + // + // - the data root is the real widget of the current data, this widget + // is not the real root that is added to the tree + // - the view root is the actual tree in the widget (should be const but cannot be + // since + // the parent tree cannot be consstructed in the member initializer list) + // + ArchiveTreeWidgetItem* m_DataRoot; + ArchiveTreeWidgetItem* m_ViewRoot; + + friend class ArchiveTreeWidgetItem; +}; + +#endif // ARCHIVETREE_H diff --git a/libs/installer_manual/src/installdialog.cpp b/libs/installer_manual/src/installdialog.cpp new file mode 100644 index 0000000..a6e96b4 --- /dev/null +++ b/libs/installer_manual/src/installdialog.cpp @@ -0,0 +1,215 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "installdialog.h" +#include "ui_installdialog.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace MOBase; + +InstallDialog::InstallDialog( + std::shared_ptr tree, const GuessedValue& modName, + std::shared_ptr modDataChecker, + const QString& dataName, QWidget* parent) + : TutorableDialog("InstallDialog", parent), ui(new Ui::InstallDialog), + m_Checker(modDataChecker), m_DataFolderName(dataName) +{ + + ui->setupUi(this); + + for (auto iter = modName.variants().begin(); iter != modName.variants().end(); + ++iter) { + ui->nameCombo->addItem(*iter); + } + + ui->nameCombo->setCurrentIndex(ui->nameCombo->findText(modName)); + ui->nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); + + m_ProblemLabel = ui->problemLabel; + + m_Tree = ui->treeContent; + m_TreeRoot = new ArchiveTreeWidgetItem(tree); + m_Tree->setup(m_DataFolderName); + connect(m_Tree, &ArchiveTreeWidget::treeChanged, [this] { + updateProblems(); + }); + + m_Tree->setDataRoot(m_TreeRoot); +} + +InstallDialog::~InstallDialog() +{ + delete ui; +} + +QString InstallDialog::getModName() const +{ + return ui->nameCombo->currentText(); +} + +/** + * @brief Retrieve the user-modified directory structure. + * + * @return the new tree represented by this dialog, which can be a new + * tree or a subtree of the original tree. + **/ +std::shared_ptr InstallDialog::getModifiedTree() const +{ + return m_Tree->root()->entry()->astree(); +} + +bool InstallDialog::testForProblem() +{ + if (!m_Checker) { + return true; + } + return m_Checker->dataLooksValid(m_Tree->root()->entry()->astree()) == + ModDataChecker::CheckReturn::VALID; +} + +void InstallDialog::updateProblems() +{ + if (!m_Checker) { + m_Tree->setStyleSheet("QTreeWidget { border: none; }"); + m_ProblemLabel->setText( + tr("Cannot check the content of <%1>.").arg(m_DataFolderName)); + m_ProblemLabel->setToolTip(tr("The plugin for the current game does not provide a " + "way to check the content of <%1>.") + .arg(m_DataFolderName)); + m_ProblemLabel->setStyleSheet("color: darkYellow;"); + } else if (testForProblem()) { + m_Tree->setStyleSheet( + "QTreeWidget { border: 1px solid darkGreen; border-radius: 2px; }"); + m_ProblemLabel->setText( + tr("The content of <%1> looks valid.").arg(m_DataFolderName)); + m_ProblemLabel->setToolTip( + tr("The content of <%1> seems valid for the current game.") + .arg(m_DataFolderName)); + m_ProblemLabel->setStyleSheet("color: darkGreen;"); + } else { + m_Tree->setStyleSheet("QTreeWidget { border: 1px solid red; border-radius: 2px; }"); + m_ProblemLabel->setText( + tr("The content of <%1> does not look valid.").arg(m_DataFolderName)); + m_ProblemLabel->setToolTip( + tr("The content of <%1> is probably not valid for the current game.") + .arg(m_DataFolderName)); + m_ProblemLabel->setStyleSheet("color: red;"); + } +} + +void InstallDialog::createDirectoryUnder(ArchiveTreeWidgetItem* item) +{ + // Should never happen if we customize the context menu depending + // on the item: + if (!item->entry()->isDir()) { + reportError(tr("Cannot create directory under a file.")); + return; + } + + // Retrieve the directory: + auto fileTree = item->entry()->astree(); + + bool ok = false; + QString result = QInputDialog::getText(this, tr("Enter a directory name"), tr("Name"), + QLineEdit::Normal, QString(), &ok); + result = result.trimmed(); + + if (ok && !result.isEmpty()) { + + // If a file with this name already exists: + if (fileTree->exists(result)) { + reportError(tr("A directory or file with that name already exists.")); + return; + } + + item->setExpanded(true); + auto* newItem = m_Tree->addDirectory(item, result); + m_Tree->scrollToItem(newItem); + } +} + +void InstallDialog::on_treeContent_customContextMenuRequested(QPoint pos) +{ + ArchiveTreeWidgetItem* selectedItem = + static_cast(m_Tree->itemAt(pos)); + if (selectedItem == nullptr) { + return; + } + + QMenu menu; + + if (selectedItem != m_Tree->root() && selectedItem->entry()->isDir()) { + menu.addAction(tr("Set as <%1> directory").arg(m_DataFolderName), + [this, selectedItem]() { + m_Tree->setDataRoot(selectedItem); + }); + } + + if (m_Tree->root()->entry() != m_TreeRoot->entry()) { + menu.addAction(tr("Unset <%1> directory").arg(m_DataFolderName), [this]() { + m_Tree->setDataRoot(m_TreeRoot); + }); + } + + // Add a separator if not empty: + if (!menu.isEmpty()) { + menu.addSeparator(); + } + + if (selectedItem->entry()->isDir()) { + menu.addAction(tr("Create directory..."), [this, selectedItem]() { + createDirectoryUnder(selectedItem); + }); + } else { + menu.addAction(tr("&Open"), [this, selectedItem]() { + emit openFile(selectedItem->entry().get()); + }); + } + menu.exec(m_Tree->mapToGlobal(pos)); +} + +void InstallDialog::on_okButton_clicked() +{ + if (!testForProblem()) { + if (QMessageBox::question( + this, tr("Continue?"), + tr("This mod was probably NOT set up correctly, most likely it will NOT " + "work. " + "You should first correct the directory layout using the content-tree."), + QMessageBox::Ignore | QMessageBox::Cancel, + QMessageBox::Cancel) == QMessageBox::Cancel) { + return; + } + } + this->accept(); +} + +void InstallDialog::on_cancelButton_clicked() +{ + this->reject(); +} diff --git a/libs/installer_manual/src/installdialog.h b/libs/installer_manual/src/installdialog.h new file mode 100644 index 0000000..96e4085 --- /dev/null +++ b/libs/installer_manual/src/installdialog.h @@ -0,0 +1,124 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef INSTALLDIALOG_H +#define INSTALLDIALOG_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "archivetree.h" + +namespace Ui +{ +class InstallDialog; +} + +/** + * a dialog presented to manually define how a mod is to be installed. It provides + * a tree view of the file contents that can modified directly + **/ +class InstallDialog : public MOBase::TutorableDialog +{ + Q_OBJECT + +public: + /** + * @brief Create a new install dialog for the given tree. The tree + * is "own" by the dialog, i.e., any change made by the user is immediately + * reflected to the given tree, except for the changes to the root. + * + * @param tree Tree structure describing the original archive structure. + * @param modName Name of the mod. The name can be modified through the dialog. + * @param modDataChecker The mod data checker to use to check. + * @param dataName The name of the data folder for the game. + * @param parent Parent widget. + **/ + explicit InstallDialog(std::shared_ptr tree, + const MOBase::GuessedValue& modName, + std::shared_ptr modDataChecker, + const QString& dataName, QWidget* parent = 0); + ~InstallDialog(); + + /** + * @brief retrieve the (modified) mod name + * + * @return updated mod name + **/ + QString getModName() const; + + /** + * @brief Retrieve the user-modified directory structure. + * + * @return the new tree represented by this dialog, which can be a new + * tree or a subtree of the original tree. + **/ + std::shared_ptr getModifiedTree() const; + +signals: + + /** + * @brief Signal emitted when user request the file corresponding + * to the given entry to be opened. + * + * @param entry Entry corresponding to the file to open. + */ + void openFile(const MOBase::FileTreeEntry* entry); + +private: + bool testForProblem(); + void updateProblems(); + void createDirectoryUnder(ArchiveTreeWidgetItem* treeItem); + +private slots: + + // Automatic slots that are directly bound to the UI: + void on_treeContent_customContextMenuRequested(QPoint pos); + void on_cancelButton_clicked(); + void on_okButton_clicked(); + +private: + Ui::InstallDialog* ui; + + std::shared_ptr m_Checker; + + // Name of the "data" directory: + QString m_DataFolderName; + + // the tree root is the initial root that will never change (should be const + // but cannot be since the parent tree cannot be constructed in the member + // initializer list) + // + // the tree root is not actually added to the tree, but is used to maintain + // the state of the tree and not lose entries when unsetting data root + // + ArchiveTreeWidget* m_Tree; + ArchiveTreeWidgetItem* m_TreeRoot; + QLabel* m_ProblemLabel; +}; + +#endif // INSTALLDIALOG_H diff --git a/libs/installer_manual/src/installdialog.ui b/libs/installer_manual/src/installdialog.ui new file mode 100644 index 0000000..49a7fdc --- /dev/null +++ b/libs/installer_manual/src/installdialog.ui @@ -0,0 +1,162 @@ + + + InstallDialog + + + + 0 + 0 + 516 + 407 + + + + + 16777215 + 16777215 + + + + Install Mods + + + + + + false + + + + + + + + + + + + + + + 50 + 0 + + + + Name + + + + + + + true + + + + + + + + + Content + + + + + + + Qt::CustomContextMenu + + + Content of the archive. You can change the directory structure by using drag&drop. Hint: Also try right clicking... + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This displays the content of the archive. &lt;data&gt; represents the base directory which will map to the game's data directory. You can change the base directory via the right-click context menu and you can move around files via drag&amp;drop</span></p></body></html> + + + + + + true + + + QAbstractItemView::DragDrop + + + QAbstractItemView::ExtendedSelection + + + false + + + + 1 + + + + + + + + + + + + + + 8 + 75 + true + + + + Placeholder + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + OK + + + + + + + Cancel + + + + + + + + + + ArchiveTreeWidget + QTreeWidget +
archivetree.h
+
+
+ + +
diff --git a/libs/installer_manual/src/installer_manual_en.ts b/libs/installer_manual/src/installer_manual_en.ts new file mode 100644 index 0000000..3bc22c1 --- /dev/null +++ b/libs/installer_manual/src/installer_manual_en.ts @@ -0,0 +1,164 @@ + + + + + ArchiveTreeWidget + + + + Cannot drop + + + + + Cannot drop '%1' into one of its subfolder. + + + + + A file '%1' already exists in folder '%2'. + + + + + A folder '%1' already exists in folder '%2'. + + + + + InstallDialog + + + Install Mods + + + + + + Name + + + + + Content + + + + + Content of the archive. You can change the directory structure by using drag&drop. Hint: Also try right clicking... + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This displays the content of the archive. &lt;data&gt; represents the base directory which will map to the game's data directory. You can change the base directory via the right-click context menu and you can move around files via drag&amp;drop</span></p></body></html> + + + + + Placeholder + + + + + OK + + + + + Cancel + + + + + Enter a directory name + + + + + Cannot check the content of <%1>. + + + + + The plugin for the current game does not provide a way to check the content of <%1>. + + + + + The content of <%1> looks valid. + + + + + The content of <%1> seems valid for the current game. + + + + + The content of <%1> does not look valid. + + + + + The content of <%1> is probably not valid for the current game. + + + + + Cannot create directory under a file. + + + + + A directory or file with that name already exists. + + + + + Set as <%1> directory + + + + + Unset <%1> directory + + + + + Create directory... + + + + + &Open + + + + + Continue? + + + + + This mod was probably NOT set up correctly, most likely it will NOT work. You should first correct the directory layout using the content-tree. + + + + + InstallerManual + + + Manual Installer + + + + + Fallback installer for mods that can be extracted but can't be handled by another installer + + + + diff --git a/libs/installer_manual/src/installermanual.cpp b/libs/installer_manual/src/installermanual.cpp new file mode 100644 index 0000000..278b5f2 --- /dev/null +++ b/libs/installer_manual/src/installermanual.cpp @@ -0,0 +1,136 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "installermanual.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "installdialog.h" + +using namespace MOBase; + +InstallerManual::InstallerManual() : m_MOInfo(nullptr) {} + +bool InstallerManual::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + return true; +} + +QString InstallerManual::name() const +{ + return "Manual Installer"; +} + +QString InstallerManual::localizedName() const +{ + return tr("Manual Installer"); +} + +QString InstallerManual::author() const +{ + return "Tannin, Holt59"; +} + +QString InstallerManual::description() const +{ + return tr("Fallback installer for mods that can be extracted but can't be handled by " + "another installer"); +} + +VersionInfo InstallerManual::version() const +{ + return VersionInfo(1, 0, 1, VersionInfo::RELEASE_FINAL); +} + +QList InstallerManual::settings() const +{ + return QList(); +} + +unsigned int InstallerManual::priority() const +{ + return 0; +} + +bool InstallerManual::isManualInstaller() const +{ + return true; +} + +bool InstallerManual::isArchiveSupported(std::shared_ptr) const +{ + return true; +} + +void InstallerManual::openFile(const FileTreeEntry* entry) +{ + QString tempName = manager()->extractFile(entry->shared_from_this()); + +#ifdef _WIN32 + SHELLEXECUTEINFOW execInfo; + memset(&execInfo, 0, sizeof(SHELLEXECUTEINFOW)); + execInfo.cbSize = sizeof(SHELLEXECUTEINFOW); + execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; + execInfo.lpVerb = L"open"; + std::wstring fileNameW = ToWString(tempName); + execInfo.lpFile = fileNameW.c_str(); + execInfo.nShow = SW_SHOWNORMAL; + if (!::ShellExecuteExW(&execInfo)) { + qCritical("failed to spawn %s: %d", qUtf8Printable(tempName), ::GetLastError()); + } +#else + if (!QDesktopServices::openUrl(QUrl::fromLocalFile(tempName))) { + qCritical("failed to open %s", qUtf8Printable(tempName)); + } +#endif +} + +IPluginInstaller::EInstallResult +InstallerManual::install(GuessedValue& modName, + std::shared_ptr& tree, QString&, int&) +{ + qDebug("offering installation dialog"); + InstallDialog dialog( + tree, modName, m_MOInfo->gameFeatures()->gameFeature(), + m_MOInfo->managedGame()->dataDirectory().dirName().toLower(), parentWidget()); + connect(&dialog, &InstallDialog::openFile, this, &InstallerManual::openFile); + if (dialog.exec() == QDialog::Accepted) { + modName.update(dialog.getModName(), GUESS_USER); + + // TODO probably more complicated than necessary + tree = dialog.getModifiedTree(); + return IPluginInstaller::RESULT_SUCCESS; + } else { + return IPluginInstaller::RESULT_CANCELED; + } +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(installerManual, InstallerManual) +#endif diff --git a/libs/installer_manual/src/installermanual.h b/libs/installer_manual/src/installermanual.h new file mode 100644 index 0000000..78bbd7f --- /dev/null +++ b/libs/installer_manual/src/installermanual.h @@ -0,0 +1,72 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef INSTALLERMANUAL_H +#define INSTALLERMANUAL_H + +#include +#include + +class InstallerManual : public MOBase::IPluginInstallerSimple +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.InstallerManual") +#endif + +public: + InstallerManual(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + + virtual unsigned int priority() const; + virtual bool isManualInstaller() const; + + virtual bool isArchiveSupported(std::shared_ptr tree) const; + virtual EInstallResult install(MOBase::GuessedValue& modName, + std::shared_ptr& tree, + QString& version, int& modID); + +private: + bool + isSimpleArchiveTopLayer(const std::shared_ptr tree) const; + std::shared_ptr + getSimpleArchiveBase(const std::shared_ptr tree) const; + +private slots: + + /** + * @brief Opens a file from the archive in the (system-)default editor/viewer. + * + * @param entry Entry corresponding to the file to open. + */ + void openFile(const MOBase::FileTreeEntry* entry); + +private: + const MOBase::IOrganizer* m_MOInfo; +}; + +#endif // INSTALLERMANUAL_H diff --git a/libs/installer_manual/vcpkg.json b/libs/installer_manual/vcpkg.json new file mode 100644 index 0000000..28051ad --- /dev/null +++ b/libs/installer_manual/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673" + } + } +} diff --git a/libs/installer_omod/.editorconfig b/libs/installer_omod/.editorconfig new file mode 100644 index 0000000..16be790 --- /dev/null +++ b/libs/installer_omod/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*.cpp] +indent_style = space +indent_size = 2 +insert_final_newline = true + +[*.h] +indent_style = space +indent_size = 2 +insert_final_newline = true + +[*.ui] +indent_style = space +indent_size = 2 +insert_final_newline = true diff --git a/libs/installer_omod/.gitattributes b/libs/installer_omod/.gitattributes new file mode 100644 index 0000000..d0b87f4 --- /dev/null +++ b/libs/installer_omod/.gitattributes @@ -0,0 +1,9 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf + +*.ts text eol=lf diff --git a/libs/installer_omod/.github/workflows/build.yml b/libs/installer_omod/.github/workflows/build.yml new file mode 100644 index 0000000..1c71bdc --- /dev/null +++ b/libs/installer_omod/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build Installer OMOD + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }} + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Installer OMOD + id: build-installer-omod + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase diff --git a/libs/installer_omod/.gitignore b/libs/installer_omod/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/installer_omod/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/installer_omod/.pre-commit-config.yaml b/libs/installer_omod/.pre-commit-config.yaml new file mode 100644 index 0000000..7bd1380 --- /dev/null +++ b/libs/installer_omod/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + # - repo: https://github.com/pre-commit/mirrors-clang-format + # rev: v19.1.5 + # hooks: + # - id: clang-format + # 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/installer_omod/CMakeLists.txt b/libs/installer_omod/CMakeLists.txt new file mode 100644 index 0000000..ed45650 --- /dev/null +++ b/libs/installer_omod/CMakeLists.txt @@ -0,0 +1,9 @@ +# CMake 3.18 due to https://gitlab.kitware.com/cmake/cmake/-/issues/20764 +cmake_minimum_required(VERSION 3.18) + +# set globally as Nuget gets confused about ZERO_CHECK, ALL_BUILD and INSTALL otherwise +set(CMAKE_DOTNET_TARGET_FRAMEWORK_VERSION "v4.8") + +project(installer_omod LANGUAGES CXX CSharp) + +add_subdirectory(src) diff --git a/libs/installer_omod/CMakePresets.json b/libs/installer_omod/CMakePresets.json new file mode 100644 index 0000000..546b445 --- /dev/null +++ b/libs/installer_omod/CMakePresets.json @@ -0,0 +1,59 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_VS_NUGET_PACKAGE_RESTORE": { + "type": "BOOL", + "value": "ON" + }, + "INSTALL_REQUIRES_VS_PACKAGE_RESTORE": { + "type": "BOOL", + "value": "ON" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/installer_omod/LICENSE b/libs/installer_omod/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/libs/installer_omod/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/libs/installer_omod/src/CMakeLists.txt b/libs/installer_omod/src/CMakeLists.txt new file mode 100644 index 0000000..f28851c --- /dev/null +++ b/libs/installer_omod/src/CMakeLists.txt @@ -0,0 +1,67 @@ +# 3.23 for CMAKE_VS_NUGET_PACKAGE_RESTORE +cmake_minimum_required(VERSION 3.23) + +find_package(mo2-cmake CONFIG REQUIRED) +find_package(mo2-uibase CONFIG REQUIRED) + +# Dummy .NET library as VS_PACKAGE_REFERENCES doesn't work on C++/CLI projects yet +# Needs to be declared before cmake_common stuff is included as that polutes the environment and makes C# get compiled as C++ +add_library(dummy_cs_project SHARED DummyCSFile.cs) +set_target_properties(dummy_cs_project PROPERTIES + LINKER_LANGUAGE CSharp + VS_PACKAGE_REFERENCES "OMODFramework_2.2.2;OMODFramework.Scripting_2.2.2;RtfPipe_1.0.7388.1242" +) + +add_library(installer_omod SHARED) +mo2_configure_plugin(installer_omod WARNINGS OFF CLI ON) +target_link_libraries(installer_omod PRIVATE mo2::uibase) + +# I'd like to use get_target_property(source_files ${PROJECT_NAME} SOURCES) as +# globbing is naughty, but need to filter out the things that aren't relative to this directory. +file(GLOB_RECURSE source_files CONFIGURE_DEPENDS *.cpp;*.h;*.ui) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX src FILES ${source_files}) + +# Ideally we'd use "$", but the relevant property +# doesn't support generator expressions. +set(omod_framework_prefix "${CMAKE_BINARY_DIR}/$(Configuration)" + CACHE PATH "Path where OMODFramework.dll and OMODFramework.Scripting.dll can be found. The default value is where the nuget package artifacts will end up. Override if you want to use a custom build.") + +set_target_properties(${PROJECT_NAME} PROPERTIES + COMMON_LANGUAGE_RUNTIME "" + # latest may not work with CLR + CXX_STANDARD 20 + #DOTNET_TARGET_FRAMEWORK "netstandard2.0" + VS_DOTNET_REFERENCE_OMODFramework "${omod_framework_prefix}/OMODFramework.dll" + VS_DOTNET_REFERENCE_OMODFramework.Scripting "${omod_framework_prefix}/OMODFramework.Scripting.dll" + VS_DOTNET_REFERENCE_RtfPipe "${CMAKE_BINARY_DIR}/$(Configuration)/RtfPipe.dll" + VS_DOTNET_REFERENCES "System.dll;System.Core.dll" +) + +target_compile_options( + installer_omod + PRIVATE + # OMODFramework and OMODFramework.Scripting reference different .NET standard libraries. This generates warnings when using them together. + "/wd4691") + +# We don't want ERROR defined +target_compile_definitions(installer_omod PRIVATE "NOGDI") + +# We don't need to actually link with dummy_cs_project, especially as its dependencies +# aren't pulled in. We do need it to build first, though. +add_dependencies(installer_omod dummy_cs_project) + +mo2_install_plugin(installer_omod FOLDER) + +install( + FILES + "$/OMODFramework.dll" + "$/OMODFramework.Scripting.dll" + "$/ICSharpCode.SharpZipLib.dll" + "$/System.Drawing.Common.dll" + "$/RtfPipe.dll" + DESTINATION bin/plugins/installer_omod/ +) +install( + FILES "$/ICSharpCode.SharpZipLib.pdb" + DESTINATION pdb) +# Other PDB files get downloaded by Visual Studio during debugging when https://symbols.nuget.org/download/symbols is in the symbol server list diff --git a/libs/installer_omod/src/DummyCSFile.cs b/libs/installer_omod/src/DummyCSFile.cs new file mode 100644 index 0000000..b093f32 --- /dev/null +++ b/libs/installer_omod/src/DummyCSFile.cs @@ -0,0 +1,16 @@ +using OMODFramework; +using OMODFramework.Scripting; +using RtfPipe; + +namespace Dummy +{ + class Stub + { + static void Stub2() + { + OMOD omod = null; + IScriptFunctions scriptFunctions = null; + Rtf.ToHtml(""); + } + } +} diff --git a/libs/installer_omod/src/MessageBoxHelper.cpp b/libs/installer_omod/src/MessageBoxHelper.cpp new file mode 100644 index 0000000..29f0be3 --- /dev/null +++ b/libs/installer_omod/src/MessageBoxHelper.cpp @@ -0,0 +1,61 @@ +#include "MessageBoxHelper.h" + +#include + +MessageBoxHelper::MessageBoxHelper() +{ + moveToThread(QApplication::instance()->thread()); + + connect(this, &MessageBoxHelper::criticalMessageBoxSignal, this, &MessageBoxHelper::criticalMessageBoxSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &MessageBoxHelper::informationMessageBoxSignal, this, &MessageBoxHelper::informationMessageBoxSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &MessageBoxHelper::questionMessageBoxSignal, this, &MessageBoxHelper::questionMessageBoxSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &MessageBoxHelper::warningMessageBoxSignal, this, &MessageBoxHelper::warningMessageBoxSlot, Qt::ConnectionType::BlockingQueuedConnection); +} + +QMessageBox::StandardButton MessageBoxHelper::critical(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + QMessageBox::StandardButton response; + emit criticalMessageBoxSignal(response, parent, title, text, buttons, defaultButton); + return response; +} + +QMessageBox::StandardButton MessageBoxHelper::information(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + QMessageBox::StandardButton response; + emit informationMessageBoxSignal(response, parent, title, text, buttons, defaultButton); + return response; +} + +QMessageBox::StandardButton MessageBoxHelper::question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + QMessageBox::StandardButton response; + emit questionMessageBoxSignal(response, parent, title, text, buttons, defaultButton); + return response; +} + +QMessageBox::StandardButton MessageBoxHelper::warning(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + QMessageBox::StandardButton response; + emit warningMessageBoxSignal(response, parent, title, text, buttons, defaultButton); + return response; +} + +void MessageBoxHelper::criticalMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + standardButtonOut = QMessageBox::critical(parent, title, text, buttons, defaultButton); +} + +void MessageBoxHelper::informationMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + standardButtonOut = QMessageBox::information(parent, title, text, buttons, defaultButton); +} + +void MessageBoxHelper::questionMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + standardButtonOut = QMessageBox::question(parent, title, text, buttons, defaultButton); +} + +void MessageBoxHelper::warningMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + standardButtonOut = QMessageBox::warning(parent, title, text, buttons, defaultButton); +} diff --git a/libs/installer_omod/src/MessageBoxHelper.h b/libs/installer_omod/src/MessageBoxHelper.h new file mode 100644 index 0000000..aa6b926 --- /dev/null +++ b/libs/installer_omod/src/MessageBoxHelper.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +class MessageBoxHelper : public QObject +{ + Q_OBJECT + +public: + MessageBoxHelper(); + + QMessageBox::StandardButton critical(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + QMessageBox::StandardButton information(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + QMessageBox::StandardButton question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + QMessageBox::StandardButton warning(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + +signals: + void criticalMessageBoxSignal(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + void informationMessageBoxSignal(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + void questionMessageBoxSignal(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + void warningMessageBoxSignal(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + +private slots: + void criticalMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); + + void informationMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); + + void questionMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); + + void warningMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); +}; diff --git a/libs/installer_omod/src/OMODFrameworkWrapper.cpp b/libs/installer_omod/src/OMODFrameworkWrapper.cpp new file mode 100644 index 0000000..0a13620 --- /dev/null +++ b/libs/installer_omod/src/OMODFrameworkWrapper.cpp @@ -0,0 +1,771 @@ +#include "OMODFrameworkWrapper.h" + +using namespace cli; + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "implementations/CodeProgress.h" +#include "implementations/Logger.h" +#include "implementations/ScriptFunctions.h" + +#include "interop/QtDotNetConverters.h" +#include "interop/StdDotNetConverters.h" + +#include "newstuff/namedialog.h" +#include "newstuff/rtfPopup.h" + +#include "MessageBoxHelper.h" + +namespace fs = std::filesystem; + +// retrieve the path to the folder containing the proxy DLL +fs::path getPluginFolder() +{ + wchar_t path[MAX_PATH]; + HMODULE hm = NULL; + + if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCWSTR)&getPluginFolder, &hm) == 0) + { + return {}; + } + if (GetModuleFileName(hm, path, sizeof(path)) == 0) + { + return {}; + } + + return fs::path(path).parent_path(); +} + +// We want to search the plugin data directory for .NET DLLs +class AssemblyResolver +{ +public: + static bool getInitialised() { return sInitialised; } + + static void initialise(MOBase::IOrganizer* organizer) + { + if (sInitialised) + return; + sPluginDataPath = getPluginFolder(); + System::AppDomain::CurrentDomain->AssemblyResolve += gcnew System::ResolveEventHandler(&OnAssemblyResolve); + sInitialised = true; + } + +private: + static System::Reflection::Assembly^ OnAssemblyResolve(System::Object^ sender, System::ResolveEventArgs^ args); + + static bool sInitialised; + static QDir sPluginDataPath; +}; + +bool AssemblyResolver::sInitialised = false; +QDir AssemblyResolver::sPluginDataPath; + +System::Reflection::Assembly^ AssemblyResolver::OnAssemblyResolve(System::Object^ sender, System::ResolveEventArgs^ args) +{ + QString name = toQString(args->Name).section(',', 0, 0) + ".dll"; + if (sPluginDataPath.exists(name)) + return System::Reflection::Assembly::LoadFrom(toDotNetString(sPluginDataPath.absoluteFilePath(name))); + return nullptr; +} + +OMODFrameworkWrapper::OMODFrameworkWrapper(MOBase::IOrganizer* organizer, QWidget* parentWidget) + : mMoInfo(organizer) + , mParentWidget(parentWidget) + , mWaitDialog(make_nullptr()) +{ + try + { + AssemblyResolver::initialise(mMoInfo); + + constructorHelper(); + + connect(this, &OMODFrameworkWrapper::pickModName, this, &OMODFrameworkWrapper::pickModNameSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &OMODFrameworkWrapper::createMod, this, &OMODFrameworkWrapper::createModSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &OMODFrameworkWrapper::displayReadme, this, &OMODFrameworkWrapper::displayReadmeSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &OMODFrameworkWrapper::showWaitDialog, this, &OMODFrameworkWrapper::showWaitDialogSlot, Qt::ConnectionType::QueuedConnection); + connect(this, &OMODFrameworkWrapper::hideWaitDialog, this, &OMODFrameworkWrapper::hideWaitDialogSlot, Qt::ConnectionType::QueuedConnection); + + initFrameworkSettings(); + } + catch (const std::exception& e) + { + throw; + } + catch (System::Exception^ dotNetException) + { + throw toStdException(dotNetException); + } +} + +void OMODFrameworkWrapper::constructorHelper() +{ + // We can't call a function doing this before AssemblyResolver::initialise happens as the DLL needs to be available before its stack frame is created. + mTempPathStack.push(toQString(OMODFramework::Framework::Settings->TempPath)); +} + +ref class InstallInAnotherThreadHelper +{ +public: + InstallInAnotherThreadHelper(OMODFrameworkWrapper* owner, MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) + : mModName(&modName) + , mGameName(&gameName) + , mArchiveName(&archiveName) + , mVersion(&version) + , mNexusID(nexusID) + , mOwner(owner) + , mExceptionPtr(new std::exception_ptr) + , mHasResult(false) + {} + + ~InstallInAnotherThreadHelper() + { + if (!mExceptionPtr) + return; + + this->!InstallInAnotherThreadHelper(); + } + + !InstallInAnotherThreadHelper() + { + delete mExceptionPtr; + mExceptionPtr = nullptr; + } + + void Run() + { + try + { + mResult = mOwner->install(*mModName, *mGameName, *mArchiveName, *mVersion, mNexusID); + mHasResult = true; + } + catch (...) + { + *mExceptionPtr = std::current_exception(); + } + } + + bool HasResult() + { + return mHasResult; + } + + OMODFrameworkWrapper::EInstallResult Result() + { + return mResult; + } + + std::exception_ptr ExceptionPtr() + { + return *mExceptionPtr; + } + +private: + // We own none of these pointers. They're pointers because managed objects can't have unmanaged members, but a pointer is just an integer of some form, which is the same. + MOBase::GuessedValue* mModName; + QString* mGameName; + const QString* mArchiveName; + const QString* mVersion; + int mNexusID; + + OMODFrameworkWrapper* mOwner; + + std::exception_ptr* mExceptionPtr; + OMODFrameworkWrapper::EInstallResult mResult; + bool mHasResult; +}; + +OMODFrameworkWrapper::EInstallResult OMODFrameworkWrapper::installInAnotherThread(MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) +{ + QEventLoop eventLoop; + InstallInAnotherThreadHelper^ helper = gcnew InstallInAnotherThreadHelper(this, modName, gameName, archiveName, version, nexusID); + System::Threading::Tasks::Task^ installationTask = System::Threading::Tasks::Task::Run(gcnew System::Action(helper, &InstallInAnotherThreadHelper::Run)); + + // TODO: connect stuff to eventLoop.wakeUp. + // Installation manager does this with futureWatcher.finished and progressUpdate + while (!installationTask->IsCompleted) + eventLoop.processEvents(QEventLoop::AllEvents | QEventLoop::WaitForMoreEvents); + + if (helper->HasResult()) + return helper->Result(); + else if (helper->ExceptionPtr()) + std::rethrow_exception(helper->ExceptionPtr()); + else + throw std::runtime_error("Something went horribly wrong when asynchronously installing an OMOD. We don't even have the original exception."); +} + +// TODO: replace with std::scope_exit when it leaves std::experimental +template +class scope_guard +{ +public: + scope_guard(T onExit) : mOnExit(onExit) {} + ~scope_guard() { mOnExit(); } +private: + T mOnExit; +}; + +OMODFrameworkWrapper::EInstallResult OMODFrameworkWrapper::install(MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) +{ + try + { + QObject_unique_ptr messageBoxHelper = make_unique(); + + MOBase::log::debug("Installing {} as OMOD", archiveName); + + emit showWaitDialog("Initializing OMOD installer... "); + refreshFrameworkSettings(); + // Stack allocating should dispose like a `using` statement in C# + OMODFramework::OMOD omod(toDotNetString(archiveName)); + emit hideWaitDialog(); + + if (!System::String::IsNullOrEmpty(omod.ModName)) + modName.update(toQString(omod.ModName), MOBase::EGuessQuality::GUESS_META); + + bool nameNotCancelled; + emit pickModName(nameNotCancelled, modName); + if (!nameNotCancelled) + return EInstallResult::RESULT_CANCELED; + + MOBase::IModInterface* modInterface; + emit createMod(modInterface, modName); + if (!modInterface) + return EInstallResult::RESULT_CANCELED; + + { + QTemporaryDir tempPath(modInterface->absolutePath() + "/OMODInstallTempXXXXXX"); + pushTempPath(tempPath.path()); + scope_guard tempPathGuard([this]() { this->popTempPath(); }); + + if (omod.HasReadme) + emit displayReadme(toQString(omod.ModName), toQString(omod.GetReadme())); + + if (omod.HasScript) + { + MOBase::log::debug("Mod has script. Run it."); + OMODFramework::Scripting::IScriptFunctions^ scriptFunctions = gcnew ScriptFunctions(mParentWidget, mMoInfo); + + System::Collections::Generic::List^ activeBSAs = gcnew System::Collections::Generic::List(); + { + // Hopefully some other part of MO2 also knows this and this can be refactored + auto dataArchives = mMoInfo->gameFeatures()->gameFeature(); + // force-enabled by engine + auto bsas = dataArchives->vanillaArchives(); + // explicitly enabled in INI. For Oblivion, excludes the vanilla BSAs by default. + bsas.append(dataArchives->archives(mMoInfo->profile().get())); + for (const auto& bsa : bsas) + { + QString path = mMoInfo->resolvePath(bsa); + if (!path.isEmpty()) + activeBSAs->Add(toDotNetString(path)); + } + + std::map loadOrder; + for (const auto& plugin : mMoInfo->pluginList()->pluginNames()) + { + int loadIndex = mMoInfo->pluginList()->loadOrder(plugin); + if (loadIndex != -1) + loadOrder[loadIndex] = plugin; + } + for (const auto& [loadIndex, plugin] : loadOrder) + { + QString bsaPath = QFileInfo(plugin).completeBaseName() + ".bsa"; + bsaPath = mMoInfo->resolvePath(bsaPath); + if (!bsaPath.isEmpty()) + activeBSAs->Add(toDotNetString(bsaPath)); + } + } + OMODFramework::Framework::LoadBSAs(activeBSAs); + scope_guard bsaGuard([]() { OMODFramework::Framework::ClearBSAs(); }); + + System::String^ dataPath = omod.GetDataFiles(); + System::String^ pluginsPath = omod.GetPlugins(); + OMODFramework::ScriptReturnData^ scriptData = OMODFramework::Scripting::ScriptRunner::RunScript(%omod, scriptFunctions, dataPath, pluginsPath); + if (!scriptData) + throw std::runtime_error("OMOD script returned no result. This isn't supposed to happen."); + if (scriptData->CancelInstall) + return EInstallResult::RESULT_CANCELED; + + // inis first so that you don't need to wait for extraction before a second batch of questions appears + if (scriptData->INIEdits && scriptData->INIEdits->Count) + { + QMap> iniEdits; + for each (OMODFramework::INIEditInfo ^ edit in scriptData->INIEdits) + iniEdits[toQString(edit->Section)][toQString(edit->Name)] = toQString(edit->NewValue); + // This feels like something I shouldn't need to do manually + QVariantMap iniEditsVariants; + for (const auto& section : iniEdits.keys()) + { + QVariantMap innerMap; + for (const auto& setting : iniEdits[section]) + innerMap[setting] = iniEdits[section][setting]; + iniEditsVariants[section] = innerMap; + } + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".iniEdits", iniEditsVariants); + + QString oblivionIniPath = mMoInfo->profile()->absoluteIniFilePath("Oblivion.ini"); + bool yesToAll = false; + for each (OMODFramework::INIEditInfo ^ edit in scriptData->INIEdits) + { + QString section = toQString(edit->Section); + section = section.mid(1, section.size() - 2); + QString name = toQString(edit->Name); + QString newValue = toQString(edit->NewValue); + QString oldValue; + if (edit->OldValue) + oldValue = toQString(edit->OldValue); + else + { + // I'm pretty sure this is the maximum length for vanilla Oblivion. + wchar_t buffer[256]; + if (GetPrivateProfileString(section.toStdWString().data(), name.toStdWString().data(), nullptr, buffer, sizeof(buffer) / sizeof(buffer[0]), oblivionIniPath.toStdWString().data())) + oldValue = QString::fromWCharArray(buffer); + } + + MOBase::log::debug("OMOD wants to set [{}] {} to \"{}\", was \"{}\"", section, name, newValue, oldValue); + + if (oldValue == newValue) + { + MOBase::log::debug("Value is unchanged, not nagging user."); + continue; + } + + QMessageBox::StandardButton response; + if (!yesToAll) + { + QString message; + if (!oldValue.isEmpty()) + { + /*: %1 is the mod name + [%2] is the ini section name. + %3 is the ini setting name. + %4 is the value already in Oblivion.ini. + %5 is the value the mod wants to set. + */ + message = tr("%1 wants to change [%2] %3 from \"%4\" to \"%5\"").arg(modName).arg(section).arg(name).arg(oldValue).arg(newValue); + } + else + { + /*: %1 is the mod name + [%2] is the ini section name. + %3 is the ini setting name. + %5 is the value the mod wants to set. + */ + message = tr("%1 wants to set [%2] %3 to \"%4\"").arg(modName).arg(section).arg(name).arg(newValue); + } + + response = messageBoxHelper->question(mParentWidget, tr("Update INI?"), message, QMessageBox::Yes | QMessageBox::No | QMessageBox::YesToAll | QMessageBox::NoToAll); + if (response == QMessageBox::NoToAll) + { + MOBase::log::debug("User skipped all."); + break; + } + + yesToAll |= response == QMessageBox::YesToAll; + } + + if (yesToAll || response == QMessageBox::StandardButton::Yes) + { + MOBase::log::debug("Doing edit."); + MOBase::WriteRegistryValue(section.toStdWString().data(), name.toStdWString().data(), newValue.toStdWString().data(), oblivionIniPath.toStdWString().data()); + } + else + MOBase::log::debug("User skipped edit."); + } + } + + scriptData->Pretty(%omod, dataPath, pluginsPath); + // no compatability between auto and var makes me :angery: + System::Collections::Generic::HashSet^ installedPlugins = gcnew System::Collections::Generic::HashSet(System::StringComparer::InvariantCultureIgnoreCase); + for each (OMODFramework::InstallFile file in scriptData->InstallFiles) + { + System::String^ destinationPath = System::IO::Path::Combine(toDotNetString(modInterface->absolutePath()), file.InstallTo); + System::IO::Directory::CreateDirectory(System::IO::Path::GetDirectoryName(destinationPath)); + System::IO::File::Copy(file.InstallFrom, destinationPath, true); + System::String^ extension = System::IO::Path::GetExtension(file.InstallTo); + if (extension && (extension->Equals(".esm", System::StringComparison::InvariantCultureIgnoreCase) || extension->Equals(".esp", System::StringComparison::InvariantCultureIgnoreCase))) + installedPlugins->Add(file.InstallTo); + } + + if (scriptData->UncheckedPlugins) + installedPlugins->ExceptWith(scriptData->UncheckedPlugins); + + QStringList defaultActivePlugins; + for each (System::String ^ plugin in installedPlugins) + defaultActivePlugins.append(toQString(plugin)); + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".defaultActivePlugins", defaultActivePlugins); + QStringList defaultInactivePlugins; + if (scriptData->UncheckedPlugins) + { + for each (System::String ^ plugin in scriptData->UncheckedPlugins) + defaultInactivePlugins.append(toQString(plugin)); + } + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".defaultInactivePlugins", defaultInactivePlugins); + + QStringList registeredBSAs; + if (scriptData->RegisterBSASet) + { + for each (System::String ^ bsa in scriptData->RegisterBSASet) + registeredBSAs.append(toQString(bsa)); + } + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".registeredBSAs", registeredBSAs); + + if (scriptData->SDPEdits && scriptData->SDPEdits->Count) + { + for each (OMODFramework::SDPEditInfo ^ shaderEdit in scriptData->SDPEdits) + { + System::String^ destinationPath = System::IO::Path::Combine(toDotNetString(modInterface->absolutePath()), "Shaders", "OMOD", toDotNetString(QString::number(shaderEdit->Package))); + System::IO::Directory::CreateDirectory(destinationPath); + System::IO::File::Copy(shaderEdit->BinaryObject, System::IO::Path::Combine(destinationPath, shaderEdit->Shader), true); + } + } + + std::map unhandledScriptReturnDataCounts; + // This is a mapping from plugin name to an enum saying whether OBMM should allow the user to deactivate an ESP from the OMOD, disallow it, or just warn. By default, it'd warn. + unhandledScriptReturnDataCounts["ESPDeactivation"] = scriptData->ESPDeactivation ? scriptData->ESPDeactivation->Count : 0; + // There's nothing in the OBMM documentation claiming the function that sets this exists. + unhandledScriptReturnDataCounts["EarlyPlugins"] = scriptData->EarlyPlugins ? scriptData->EarlyPlugins->Count : 0; + // Sets load order a.esp, b.esp, true and b.esp, a.esp, false both mean the same thing. + unhandledScriptReturnDataCounts["LoadOrderSet"] = scriptData->LoadOrderSet ? scriptData->LoadOrderSet->Count : 0; + // Says this OMOD conflicts with another, potentially with a description. + unhandledScriptReturnDataCounts["ConflictsWith"] = scriptData->ConflictsWith ? scriptData->ConflictsWith->Count : 0; + // Says this OMOD depends on another, potentially with a description. + unhandledScriptReturnDataCounts["DependsOn"] = scriptData->DependsOn ? scriptData->DependsOn->Count : 0; + + // Contains a list of GMSTs and Globals to edit in the mod's ESPs. May a higher being have mercy on us if anyone ever used this. + unhandledScriptReturnDataCounts["ESPEdits"] = scriptData->ESPEdits ? scriptData->ESPEdits->Count : 0; + // OMODFramework is handling this for us, so don't sweat it. + unhandledScriptReturnDataCounts["PatchFiles"] = scriptData->PatchFiles ? scriptData->PatchFiles->Count : 0; + + for (const auto& [name, usageCount] : unhandledScriptReturnDataCounts) + { + if (usageCount) + { + /*: %1 is the mod name + %2 is the name of a field in the OMOD's return data + Hopefully this message will never be seen by anyone, but if it is, they need to know to tell the Mod Organizer 2 dev team. + */ + QString userMessage = tr("%1 has data for %2, but Mod Organizer 2 doesn't know what to do with it yet. Please report this to the Mod Organizer 2 development team (ideally by sending us your interface log) as we didn't find any OMODs that actually did this, and we need to know that they exist."); + userMessage = userMessage.arg(toQString(omod.ModName)); + userMessage = userMessage.arg(name); + messageBoxHelper->warning(mParentWidget, tr("Mod Organizer 2 can't completely install this OMOD."), userMessage); + MOBase::log::warn("{} ({}) contains {} entries for {}", toUTF8String(omod.ModName), archiveName, usageCount, name); + } + } + } + else + { + MOBase::log::debug("Mod has no script. Install contents directly."); + QString data = toQString(omod.GetDataFiles()); + QString plugins = toQString(omod.GetPlugins()); + if (!data.isNull()) + { + if (MOBase::shellMove(data + "/*.*", modInterface->absolutePath(), true, mParentWidget)) + MOBase::log::debug("Installed mod files."); + else + MOBase::log::error("Error while installing mod files."); + QFile::remove(data); + } + if (!plugins.isNull()) + { + QStringList defaultActivePlugins = QDir(plugins).entryList({ "*.esm", "*.esp" }, QDir::Files); + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".defaultActivePlugins", defaultActivePlugins); + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".defaultInactivePlugins", QStringList()); + if (MOBase::shellMove(plugins + "/*.*", modInterface->absolutePath(), true, mParentWidget)) + MOBase::log::debug("Installed mod plugins."); + else + MOBase::log::error("Error while installing mod plugins."); + QFile::remove(plugins); + } + } + + // on success, set mod info + MOBase::VersionInfo modVersion(std::max(int(omod.MajorVersion), 0), std::max(int(omod.MinorVersion), 0), std::max(int(omod.BuildVersion), 0)); + modInterface->setVersion(modVersion); + + // TODO: parse omod.Website. If it's Nexus, set the ID, otherwise set custom URL in meta.ini. We can't set the URL with the installation manager. + // TODO: maybe convert omod.Description to HTML and set it as nexusDescription + // TODO: maybe Holt will finish the proposed mod metadata API and there'll be a better, tidier option. + if (nexusID != -1) + modInterface->setNexusID(nexusID); + + modInterface->setInstallationFile(archiveName); + } + + OMODFramework::Framework::CleanTempDir(false); + + QStringList omodsPendingPostInstall = modInterface->pluginSetting("Omod Installer", "omodsPendingPostInstall", QStringList()).toStringList(); + if (!omodsPendingPostInstall.contains(toQString(omod.ModName))) + { + omodsPendingPostInstall.append(toQString(omod.ModName)); + modInterface->setPluginSetting("Omod Installer", "omodsPendingPostInstall", omodsPendingPostInstall); + } + return EInstallResult::RESULT_SUCCESS; + } + catch (const std::exception& e) + { + throw; + } + catch (System::Exception^ dotNetException) + { + throw toStdException(dotNetException); + } +} + +void OMODFrameworkWrapper::setParentWidget(QWidget* parentWidget) +{ + mParentWidget = parentWidget; + if (OMODFramework::Framework::Settings->CodeProgress) + static_cast(OMODFramework::Framework::Settings->CodeProgress)->setParentWidget(mParentWidget); +} + +const std::array pluginStateNames = { "STATE_MISSING", "STATE_INACTIVE", "STATE_ACTIVE" }; + +void OMODFrameworkWrapper::onInstallationEnd(EInstallResult status, MOBase::IModInterface* mod) +{ + if (status != EInstallResult::RESULT_SUCCESS || !mod) + return; + + QStringList omodsPendingPostInstall = mod->pluginSetting("Omod Installer", "omodsPendingPostInstall", QStringList()).toStringList(); + + if (!omodsPendingPostInstall.empty() && !(mMoInfo->modList()->state(mod->name()) & MOBase::IModList::STATE_ACTIVE)) + { + auto response = QMessageBox::question(mParentWidget, tr("Activate mod?"), + /*: %1 is the left-pane mod name. + %2 is the name from the metadata of an OMOD. + */ + tr("%1 contains the OMOD %2. OMODs may have post-installation actions like activating ESPs. Would you like to enable the mod so this can happen now?").arg(mod->name()).arg(omodsPendingPostInstall[0]), + QMessageBox::Yes | QMessageBox::No); + + if (response == QMessageBox::StandardButton::Yes) + mMoInfo->modList()->setActive(mod->name(), true); + else + return; + } + + for (const auto& omodName : omodsPendingPostInstall) + { + MOBase::log::debug("Running post-install for {}", omodName); + QStringList defaultActivePlugins = mod->pluginSetting("Omod Installer", omodName + ".defaultActivePlugins", QStringList()).toStringList(); + QStringList defaultInactivePlugins = mod->pluginSetting("Omod Installer", omodName + ".defaultInactivePlugins", QStringList()).toStringList(); + + bool yesToAll = false; + for (const auto& plugin : defaultActivePlugins) + { + MOBase::IPluginList::PluginStates oldState = mMoInfo->pluginList()->state(plugin); + MOBase::log::debug("OMOD wants to activate {}, was {}", plugin, pluginStateNames[oldState]); + + QMessageBox::StandardButton response; + if (!yesToAll) + { + QString message; + if (oldState == MOBase::IPluginList::STATE_INACTIVE) + { + /*: %1 is the mod name. + %2 is the plugin name. + */ + message = tr("%1 wants to activate %2. Do you want to do so?").arg(omodName).arg(plugin); + + response = QMessageBox::question(mParentWidget, tr("Activate plugin?"), message, QMessageBox::Yes | QMessageBox::No | QMessageBox::YesToAll | QMessageBox::NoToAll); + if (response == QMessageBox::NoToAll) + { + MOBase::log::debug("User skipped all."); + break; + } + + yesToAll |= response == QMessageBox::YesToAll; + } + else + { + if (oldState == MOBase::IPluginList::STATE_MISSING) + QMessageBox::warning(mParentWidget, tr("OMOD wants to activate missing plugin"), tr("An OMOD wants to activate a missing plugin. This shouldn't be possible. Please report this to a MO2 developer.")); + continue; + } + } + + if (yesToAll || response == QMessageBox::StandardButton::Yes) + { + MOBase::log::debug("Activating plugin."); + mMoInfo->pluginList()->setState(plugin, MOBase::IPluginList::STATE_ACTIVE); + } + else + MOBase::log::debug("User skipped plugin."); + } + + yesToAll = false; + for (const auto& plugin : defaultInactivePlugins) + { + MOBase::IPluginList::PluginStates oldState = mMoInfo->pluginList()->state(plugin); + MOBase::log::debug("OMOD installed {}, but didn't try and activate it. State was {}", plugin, pluginStateNames[oldState]); + + QMessageBox::StandardButton response; + if (!yesToAll) + { + QString message; + if (oldState == MOBase::IPluginList::STATE_INACTIVE) + { + /*: %1 is the mod name. + %2 is the plugin name. + */ + message = tr("%1 installed %2, but doesn't activate it by default. Do you want to activate it anyway?").arg(omodName).arg(plugin); + + response = QMessageBox::question(mParentWidget, tr("Activate plugin?"), message, QMessageBox::Yes | QMessageBox::No | QMessageBox::YesToAll | QMessageBox::NoToAll); + if (response == QMessageBox::NoToAll) + { + MOBase::log::debug("User skipped all."); + break; + } + + yesToAll |= response == QMessageBox::YesToAll; + } + else + { + if (oldState == MOBase::IPluginList::STATE_MISSING) + QMessageBox::warning(mParentWidget, tr("OMOD claimed to install missing plugin"), tr("An OMOD has activation settings for a missing plugin. This shouldn't be possible. Please report this to a MO2 developer.")); + continue; + } + } + + if (yesToAll || response == QMessageBox::StandardButton::Yes) + { + MOBase::log::debug("Activating plugin."); + mMoInfo->pluginList()->setState(plugin, MOBase::IPluginList::STATE_ACTIVE); + } + else + MOBase::log::debug("User skipped plugin."); + } + + // this is still ugly. + mMoInfo->gameFeatures()->gameFeature()->writePluginLists(mMoInfo->pluginList()); + + + QStringList registeredBSAs = mod->pluginSetting("Omod Installer", omodName + ".registeredBSAs", QStringList()).toStringList(); + if (!registeredBSAs.empty()) + { + MOBase::log::debug("OMOD wants to register BSAs. We can't do that."); + QMessageBox::warning(mParentWidget, tr("Register BSAs"), + /*: %1 is the OMOD name +
  • %2
becomes a list of BSA files + */ + tr("%1 wants to register the following BSA archives, but Mod Organizer 2 can't do that yet due to technical limitations:
  • %2
For now, your options include adding the BSA names to sResourceArchiveList in the game INI, creating a dummy ESP with the same name, or extracting the BSA, all of which have drawbacks.") + .arg(omodName).arg(registeredBSAs.join("
  • ")) + ); + } + } + + mod->setPluginSetting("Omod Installer", "omodsPendingPostInstall", QStringList()); +} + +void OMODFrameworkWrapper::initFrameworkSettings() +{ + OMODFramework::Framework::Settings->CodeProgress = gcnew CodeProgress(mParentWidget); + + // This is a hack to fix an OMOD framework bug and should be removed once it's fixed. + OMODFramework::Framework::Settings->DllPath = System::IO::Path::Combine(System::IO::Path::GetDirectoryName(OMODFramework::Framework::Settings->DllPath), "OMODFramework.Scripting.dll"); + + OMODFramework::LoggingSettings^ loggingSettings = OMODFramework::Framework::Settings->LoggingSettings; + loggingSettings->LogToFile = false; + loggingSettings->LowestLoggingLevel = Logger::OMODLoggingLevel(MOBase::log::getDefault().level()); + loggingSettings->Logger = gcnew Logger(); + loggingSettings->UseLogger = true; + + OMODFramework::ScriptExecutionSettings^ scriptSettings = gcnew OMODFramework::ScriptExecutionSettings(); + scriptSettings->EnableWarnings = true; + scriptSettings->ReadINIWithInterface = false; + scriptSettings->ReadRendererInfoWithInterface = false; + scriptSettings->HandleBSAsWithInterface = false; + scriptSettings->PatchWithInterface = false; + scriptSettings->UseSafePatching = true; + + OMODFramework::Framework::Settings->ScriptExecutionSettings = scriptSettings; +} + +void OMODFrameworkWrapper::refreshFrameworkSettings() +{ + OMODFramework::ScriptExecutionSettings^ scriptSettings = OMODFramework::Framework::Settings->ScriptExecutionSettings; + + if (scriptSettings && mMoInfo->managedGame()) + { + // the managed game isn't set during initFrameworkSettings, so only do this here + scriptSettings->OblivionGamePath = toDotNetString(mMoInfo->managedGame()->gameDirectory().path()); + scriptSettings->OblivionINIPath = toDotNetString(mMoInfo->profile()->absoluteIniFilePath("Oblivion.ini")); + scriptSettings->OblivionRendererInfoPath = System::IO::Path::Combine(toDotNetString(mMoInfo->managedGame()->documentsDirectory().path()), "RendererInfo.txt"); + } +} + +void OMODFrameworkWrapper::pushTempPath(const QString& tempPath) +{ + if (!tempPath.isEmpty()) + OMODFramework::Framework::Settings->TempPath = toDotNetString(tempPath); + mTempPathStack.push(tempPath); +} + +void OMODFrameworkWrapper::popTempPath() +{ + if (mTempPathStack.count() >= 2) + mTempPathStack.pop(); + OMODFramework::Framework::Settings->TempPath = toDotNetString(mTempPathStack.top()); +} + +void OMODFrameworkWrapper::pickModNameSlot(bool& successOut, MOBase::GuessedValue& modName) +{ + NameDialog nameDialog(modName, mParentWidget); + successOut = nameDialog.exec() == QDialog::Accepted; + if (successOut) + modName.update(nameDialog.getName(), MOBase::EGuessQuality::GUESS_USER); +} + +void OMODFrameworkWrapper::createModSlot(MOBase::IModInterface*& modInterfaceOut, MOBase::GuessedValue& modName) +{ + modInterfaceOut = mMoInfo->createMod(modName); +} + +void OMODFrameworkWrapper::displayReadmeSlot(const QString& modName, const QString& readme) +{ + if (QMessageBox::question(mParentWidget, tr("Display Readme?"), + //:
    is a line break. Translators can remove it if it makes things clearer. + tr("The Readme may explain installation options. Display it?
    It will remain visible until you close it.")) == QMessageBox::StandardButton::Yes) + { + // TODO: ideally this wouldn't be part of the same window heirarchy so that modal popups in the installer don't prevent it being moved/resized etc. + // DarNified UI's popups are modal for the whole process, so any fancy trick needs to be *here*. + RtfPopup* readmePopup = new RtfPopup(toDotNetString(readme), mParentWidget); + //: %1 is the mod name + readmePopup->setWindowTitle(tr("%1 Readme").arg(modName)); + readmePopup->show(); + readmePopup->setAttribute(Qt::WA_DeleteOnClose); + } +} + +void OMODFrameworkWrapper::showWaitDialogSlot(QString message) { + mWaitDialog.reset(new QProgressDialog(message, QString(), 0, 0, mParentWidget)); + mWaitDialog->setWindowFlags(mWaitDialog->windowFlags() & ~Qt::WindowContextHelpButtonHint & ~Qt::WindowCloseButtonHint); + mWaitDialog->setWindowModality(Qt::WindowModal); + mWaitDialog->show(); +} + +void OMODFrameworkWrapper::hideWaitDialogSlot() { + if (mWaitDialog) { + mWaitDialog->hide(); + mWaitDialog.reset(); + } +} diff --git a/libs/installer_omod/src/OMODFrameworkWrapper.h b/libs/installer_omod/src/OMODFrameworkWrapper.h new file mode 100644 index 0000000..50251f0 --- /dev/null +++ b/libs/installer_omod/src/OMODFrameworkWrapper.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include + +#include + +#include "implementations/CodeProgress.h" +#include "QObject_unique_ptr.h" + +// define this here as it's going to be used a lot by things using this class' message box wrappers. +template +T& unused_out(T&& t) { return t; } + +class OMODFrameworkWrapper : public QObject +{ + Q_OBJECT + +public: + using EInstallResult = MOBase::IPluginInstaller::EInstallResult; + + OMODFrameworkWrapper(MOBase::IOrganizer* organizer, QWidget* parentWidget = nullptr); + + EInstallResult installInAnotherThread(MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID); + + EInstallResult install(MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID); + + void setParentWidget(QWidget* parentWidget); + + void onInstallationEnd(EInstallResult status, MOBase::IModInterface* mod); + +protected: + void initFrameworkSettings(); + + void refreshFrameworkSettings(); + + void pushTempPath(const QString& tempPath); + + void popTempPath(); + +signals: + void pickModName(bool& successOut, MOBase::GuessedValue& modName); + + void createMod(MOBase::IModInterface*& modInterfaceOut, MOBase::GuessedValue& modName); + + void displayReadme(const QString& modName, const QString& readme); + + void showWaitDialog(QString message); + void hideWaitDialog(); + +protected slots: + void pickModNameSlot(bool& successOut, MOBase::GuessedValue& modName); + void createModSlot(MOBase::IModInterface*& modInterfaceOut, MOBase::GuessedValue& modName); + void displayReadmeSlot(const QString& modName, const QString& readme); + + void showWaitDialogSlot(QString message); + void hideWaitDialogSlot(); + +private: + __declspec(noinline) void constructorHelper(); + + MOBase::IOrganizer* mMoInfo; + QWidget* mParentWidget; + + QStack mTempPathStack; + + QObject_unique_ptr mWaitDialog; +}; diff --git a/libs/installer_omod/src/QObject_unique_ptr.h b/libs/installer_omod/src/QObject_unique_ptr.h new file mode 100644 index 0000000..e6f9395 --- /dev/null +++ b/libs/installer_omod/src/QObject_unique_ptr.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +static inline void deleteQObjectLater(QObject* qObject) { qObject->deleteLater(); } + +template +using QObject_unique_ptr = std::unique_ptr; + +template +QObject_unique_ptr make_unique(Args&&... args) +{ + return QObject_unique_ptr(new T(std::forward(args)...), &deleteQObjectLater); +} + +template +QObject_unique_ptr make_nullptr() +{ + return QObject_unique_ptr(nullptr, &deleteQObjectLater); +} diff --git a/libs/installer_omod/src/implementations/CodeProgress.cpp b/libs/installer_omod/src/implementations/CodeProgress.cpp new file mode 100644 index 0000000..7256592 --- /dev/null +++ b/libs/installer_omod/src/implementations/CodeProgress.cpp @@ -0,0 +1,85 @@ +#include + +#include "CodeProgress.h" + +CodeProgressHelper::CodeProgressHelper(QWidget* parentWidget) : mParentWidget{ parentWidget }, mProgressDialog(make_nullptr()) { + moveToThread(QApplication::instance()->thread()); + connect(this, &CodeProgressHelper::ShowProgressDialogSignal, this, &CodeProgressHelper::ShowProgressDialogSlot, Qt::QueuedConnection); + connect(this, &CodeProgressHelper::UpdateProgressValueSignal, this, &CodeProgressHelper::UpdateProgressValueSlot, Qt::BlockingQueuedConnection); + connect(this, &CodeProgressHelper::HideProgressDialogSignal, this, &CodeProgressHelper::HideProgressDialogSlot, Qt::QueuedConnection); + +} + +void CodeProgressHelper::ShowProgressDialog() { + emit ShowProgressDialogSignal(); +} + +void CodeProgressHelper::UpdateProgressValue(int percentage) { + emit UpdateProgressValueSignal(percentage); +} + +void CodeProgressHelper::HideProgressDialog() { + emit HideProgressDialogSignal(); +} + +void CodeProgressHelper::ShowProgressDialogSlot() { + mProgressDialog.reset(new QProgressDialog(mParentWidget)); + mProgressDialog->setWindowFlags(mProgressDialog->windowFlags() & ~Qt::WindowContextHelpButtonHint & ~Qt::WindowCloseButtonHint); + mProgressDialog->setWindowModality(Qt::WindowModal); + mProgressDialog->setCancelButton(nullptr); + mProgressDialog->setMinimum(0); + mProgressDialog->setMaximum(100); + mProgressDialog->setAutoReset(false); + mProgressDialog->setAutoClose(false); + mProgressDialog->show(); +} + +void CodeProgressHelper::UpdateProgressValueSlot(int percentage) { + mProgressDialog->setValue(percentage); +} + +void CodeProgressHelper::HideProgressDialogSlot() { + mProgressDialog->hide(); + mProgressDialog.reset(); + } + +void CodeProgress::Init(__int64 totalSize, bool compressing) +{ + System::GC::ReRegisterForFinalize(this); + + mTotalSize = totalSize; + mCompressing = compressing; + mPercentage = 0; + + mHelper = new CodeProgressHelper(mParentWidget); + mHelper->ShowProgressDialog(); +} + +void CodeProgress::SetProgress(__int64 inSize, __int64 outSize) +{ + int newPercentage = (int)(100 * inSize / (double)mTotalSize); + if (newPercentage != mPercentage) + mHelper->UpdateProgressValue(newPercentage); + mPercentage = newPercentage; +} + +CodeProgress::~CodeProgress() +{ + if (!mHelper) + return; + + mHelper->HideProgressDialog(); + this->!CodeProgress(); +} + +CodeProgress::!CodeProgress() +{ + if (mHelper) + mHelper->deleteLater(); + mHelper = nullptr; +} + +void CodeProgress::setParentWidget(QWidget* parentWidget) +{ + mParentWidget = parentWidget; +} diff --git a/libs/installer_omod/src/implementations/CodeProgress.h b/libs/installer_omod/src/implementations/CodeProgress.h new file mode 100644 index 0000000..902fccd --- /dev/null +++ b/libs/installer_omod/src/implementations/CodeProgress.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +#include "../QObject_unique_ptr.h" + +using namespace cli; + +class CodeProgressHelper : public QObject { + Q_OBJECT +public: + + CodeProgressHelper(QWidget* parentWidget); + + void ShowProgressDialog(); + void UpdateProgressValue(int percentage); + void HideProgressDialog(); + +public slots: + void ShowProgressDialogSlot(); + void UpdateProgressValueSlot(int percentage); + void HideProgressDialogSlot(); + +signals: + + void ShowProgressDialogSignal(); + void UpdateProgressValueSignal(int percentage); + void HideProgressDialogSignal(); + +private: + QWidget* mParentWidget; + QObject_unique_ptr mProgressDialog; +}; + +ref class CodeProgress : OMODFramework::ICodeProgress +{ +public: + + CodeProgress(QWidget* parentWidget) : mParentWidget(parentWidget) { } + + virtual void Init(__int64 totalSize, bool compressing); + + virtual void SetProgress(__int64 inSize, __int64 outSize); + + ~CodeProgress(); + + !CodeProgress(); + + void setParentWidget(QWidget* parentWidget); + +private: + QWidget* mParentWidget; + CodeProgressHelper* mHelper; + __int64 mTotalSize; + int mPercentage; + bool mCompressing; +}; diff --git a/libs/installer_omod/src/implementations/Logger.cpp b/libs/installer_omod/src/implementations/Logger.cpp new file mode 100644 index 0000000..2b0a0c1 --- /dev/null +++ b/libs/installer_omod/src/implementations/Logger.cpp @@ -0,0 +1,45 @@ +#include "Logger.h" + +#include "../interop/StdDotNetConverters.h" + +OMODFramework::LoggingLevel Logger::OMODLoggingLevel(MOBase::log::Levels level) +{ + switch (level) + { + default: + case MOBase::log::Debug: + return OMODFramework::LoggingLevel::DEBUG; + case MOBase::log::Info: + return OMODFramework::LoggingLevel::INFO; + case MOBase::log::Warning: + return OMODFramework::LoggingLevel::WARNING; + case MOBase::log::Error: + return OMODFramework::LoggingLevel::ERROR; + } +} + +MOBase::log::Levels Logger::MOLoggingLevel(OMODFramework::LoggingLevel level) +{ + switch (level) + { + default: + case OMODFramework::LoggingLevel::DEBUG: + return MOBase::log::Debug; + case OMODFramework::LoggingLevel::INFO: + return MOBase::log::Info; + case OMODFramework::LoggingLevel::WARNING: + return MOBase::log::Warning; + case OMODFramework::LoggingLevel::ERROR: + return MOBase::log::Error; + } +} + +void Logger::Init() +{ + //no op +} + +void Logger::Log(OMODFramework::LoggingLevel level, System::String^ message, System::DateTime time) +{ + MOBase::log::getDefault().log(MOLoggingLevel(level), "{}", toUTF8String(message)); +} diff --git a/libs/installer_omod/src/implementations/Logger.h b/libs/installer_omod/src/implementations/Logger.h new file mode 100644 index 0000000..e3d1e7e --- /dev/null +++ b/libs/installer_omod/src/implementations/Logger.h @@ -0,0 +1,17 @@ +#pragma once + +using namespace cli; + +#include + +ref class Logger : OMODFramework::ILogger +{ +public: + static OMODFramework::LoggingLevel OMODLoggingLevel(MOBase::log::Levels level); + + static MOBase::log::Levels MOLoggingLevel(OMODFramework::LoggingLevel level); + + virtual void Init(); + + virtual void Log(OMODFramework::LoggingLevel level, System::String^ message, System::DateTime time); +}; diff --git a/libs/installer_omod/src/implementations/ScriptFunctions.cpp b/libs/installer_omod/src/implementations/ScriptFunctions.cpp new file mode 100644 index 0000000..be4deb3 --- /dev/null +++ b/libs/installer_omod/src/implementations/ScriptFunctions.cpp @@ -0,0 +1,320 @@ +#include "ScriptFunctions.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../interop/QtDotNetConverters.h" +#include "../newstuff/rtfPopup.h" +#include "../oldstuff/DialogSelect.h" + +ScriptFunctionsHelper::ScriptFunctionsHelper() : mMessageBoxHelper(make_unique()) +{ + moveToThread(QApplication::instance()->thread()); + + connect(this, &ScriptFunctionsHelper::DialogSelectSignal, this, &ScriptFunctionsHelper::DialogSelectSlot, Qt::BlockingQueuedConnection); + connect(this, &ScriptFunctionsHelper::InputStringSignal, this, &ScriptFunctionsHelper::InputStringSlot, Qt::BlockingQueuedConnection); + connect(this, &ScriptFunctionsHelper::DisplayImageSignal, this, &ScriptFunctionsHelper::DisplayImageSlot, Qt::BlockingQueuedConnection); + connect(this, &ScriptFunctionsHelper::DisplayTextSignal, this, &ScriptFunctionsHelper::DisplayTextSlot, Qt::BlockingQueuedConnection); +} + +std::optional> ScriptFunctionsHelper::DialogSelect(QWidget* parent, const QString& title, const QVector& items, const QVector& descriptions, const QVector& pixmaps, bool multiSelect) +{ + std::optional> result; + emit DialogSelectSignal(result, parent, title, items, descriptions, pixmaps, multiSelect); + return result; +} + +QString ScriptFunctionsHelper::InputString(QWidget* parentWidget, const QString& title, const QString& initialText) +{ + QString text; + emit InputStringSignal(text, parentWidget, title, initialText); + return text; +} + +void ScriptFunctionsHelper::DisplayImage(QWidget* parentWidget, const QString& path, const QString& title) +{ + emit DisplayImageSignal(parentWidget, path, title); +} + +void ScriptFunctionsHelper::DisplayText(QWidget* parentWidget, const QString& path, const QString& title) +{ + emit DisplayTextSignal(parentWidget, path, title); +} + +void ScriptFunctionsHelper::InputStringSlot(QString& textOut, QWidget* parentWidget, const QString& title, const QString& initialText) +{ + textOut = QInputDialog::getText(parentWidget, title, title, QLineEdit::Normal, initialText); +} + +void ScriptFunctionsHelper::DisplayImageSlot(QWidget* parentWidget, const QString& path, const QString& title) +{ + QImageReader reader(path); + QImage image = reader.read(); + if (!image.isNull()) + { + QPixmap pixmap = QPixmap::fromImage(image); + MOBase::log::debug("image size {}, pixmap size {}", image.size(), pixmap.size()); + QDialog popup; + QLayout* layout = new QGridLayout(&popup); + popup.setLayout(layout); + FixedAspectRatioImageLabel* label = new FixedAspectRatioImageLabel(&popup); + label->setUnscaledPixmap(pixmap); + + QSize screenSize = parentWidget->screen()->availableSize(); + int maxHeight = static_cast(screenSize.height() * 0.8f); + if (pixmap.size().height() > maxHeight) + // This is approximate due to borders, label can sort out details. + popup.resize(label->widthForHeight(maxHeight), maxHeight); + + layout->addWidget(label); + popup.setWindowTitle(title); + popup.exec(); + } + else + MOBase::log::error("Unable to display {}. Error was {}: {}", path, reader.error(), reader.errorString()); +} + +void ScriptFunctionsHelper::DisplayTextSlot(QWidget* parentWidget, const QString& path, const QString& title) +{ + RtfPopup popup(toDotNetString(path), parentWidget); + popup.setWindowTitle(title); + // the size readmes are becoming automatically + popup.resize(492, 366); + popup.exec(); +} + +void ScriptFunctionsHelper::DialogSelectSlot(std::optional>& resultOut, QWidget* parent, const QString& title, const QVector& items, + const QVector& descriptions, const QVector& pixmaps, bool multiSelect) +{ + resultOut = ::DialogSelect(parent, title, items, descriptions, pixmaps, multiSelect); +} + +ScriptFunctions::ScriptFunctions(QWidget* parentWidget, MOBase::IOrganizer* moInfo) : mParentWidget(parentWidget), mMoInfo(moInfo), mHelper(new ScriptFunctionsHelper) {} + +ScriptFunctions::~ScriptFunctions() +{ + if (!mHelper) + return; + this->!ScriptFunctions(); +} + +ScriptFunctions::!ScriptFunctions() +{ + mHelper->deleteLater(); + mHelper = nullptr; +} + +void ScriptFunctions::Warn(System::String^ msg) +{ + mHelper->warning(mParentWidget, "Warning", toQString(msg)); +} + +void ScriptFunctions::Message(System::String^ msg) +{ + mHelper->information(mParentWidget, "Message", toQString(msg)); +} + +void ScriptFunctions::Message(System::String^ msg, System::String^ title) +{ + mHelper->information(mParentWidget, toQString(title), toQString(msg)); +} + +System::Collections::Generic::List^ ScriptFunctions::Select(System::Collections::Generic::List^ items, + System::String^ title, + bool isMultiSelect, + System::Collections::Generic::List^ previews, + System::Collections::Generic::List^ descriptions) +{ + QVector qItems; + qItems.reserve(items ? items->Count : 0); + QVector qPreviews; + qPreviews.reserve(previews ? previews->Count : 0); + QVector qDescriptions; + qDescriptions.reserve(descriptions ? descriptions->Count : 0); + + // Expect red squiggles. No one told intellisense about this syntax, but it's the least ugly. + if (items) + { + for each (System::String ^ item in items) + qItems.push_back(toQString(item)); + } + + if (previews) + { + for each (System::String ^ preview in previews) + qPreviews.push_back(toQString(preview)); + } + + if (descriptions) + { + for each (System::String ^ description in descriptions) + qDescriptions.push_back(toQString(description)); + } + + std::optional> qResponse = mHelper->DialogSelect(mParentWidget, toQString(title), qItems, qDescriptions, qPreviews, isMultiSelect); + if (!qResponse.has_value()) + return nullptr; + + System::Collections::Generic::List^ response = gcnew System::Collections::Generic::List(qResponse.value().length()); + for (const auto selection : qResponse.value()) + response->Add(selection); + return response; +} + +System::String^ ScriptFunctions::InputString(System::String^ title, System::String^ initialText) +{ + return toDotNetString(mHelper->InputString(mParentWidget, toQString(title), initialText ? toQString(initialText) : "")); +} + +int ScriptFunctions::DialogYesNo(System::String^ message) +{ + return mHelper->question(mParentWidget, "", toQString(message)) == QMessageBox::StandardButton::Yes; +} + +int ScriptFunctions::DialogYesNo(System::String^ message, System::String^ title) +{ + return mHelper->question(mParentWidget, toQString(title), toQString(message)) == QMessageBox::StandardButton::Yes; +} + +void ScriptFunctions::DisplayImage(System::String^ path, System::String^ title) +{ + mHelper->DisplayImage(mParentWidget, toQString(path), toQString(title)); +} + +void ScriptFunctions::DisplayText(System::String^ text, System::String^ title) +{ + mHelper->DisplayText(mParentWidget, toQString(text), toQString(title)); +} + +void ScriptFunctions::Patch(System::String^ from, System::String^ to) +{ + throw gcnew System::NotImplementedException(); +} + +System::String^ ScriptFunctions::ReadOblivionINI(System::String^ section, System::String^ name) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} + +System::String^ ScriptFunctions::ReadRendererInfo(System::String^ name) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} + +bool ScriptFunctions::DataFileExists(System::String^ path) +{ + return mMoInfo->resolvePath(toQString(path)) != ""; +} + +bool ScriptFunctions::HasScriptExtender() +{ + for (const auto& forcedLoad : mMoInfo->managedGame()->executableForcedLoads()) + { + if (forcedLoad.library().toLower().startsWith("obse")) + { + if (mMoInfo->managedGame()->gameDirectory().exists(forcedLoad.library())) + return true; + } + } + + return mMoInfo->managedGame()->gameDirectory().exists("obse_loader.exe"); +} + +bool ScriptFunctions::HasGraphicsExtender() +{ + return DataFileExists("obse\\plugins\\obge.dll"); +} + +System::Version^ ScriptFunctions::ScriptExtenderVersion() +{ + QString obsePath; + for (const auto& forcedLoad : mMoInfo->managedGame()->executableForcedLoads()) + { + if (forcedLoad.library().toLower().startsWith("obse")) + { + if (mMoInfo->managedGame()->gameDirectory().exists(forcedLoad.library())) + { + obsePath = mMoInfo->managedGame()->gameDirectory().filePath(forcedLoad.library()); + break; + } + } + } + + if (obsePath.isEmpty()) + obsePath = mMoInfo->managedGame()->gameDirectory().filePath("obse_loader.exe"); + + System::Diagnostics::FileVersionInfo^ info = System::Diagnostics::FileVersionInfo::GetVersionInfo(toDotNetString(obsePath)); + return gcnew System::Version(info->FileMajorPart, info->FileMinorPart, info->FileBuildPart, info->FilePrivatePart); +} + +System::Version^ ScriptFunctions::GraphicsExtenderVersion() +{ + return gcnew System::Version(System::Diagnostics::FileVersionInfo::GetVersionInfo(toDotNetString(mMoInfo->resolvePath("obse\\plugins\\obge.dll")))->FileVersion); +} + +System::Version^ ScriptFunctions::OblivionVersion() +{ + return gcnew System::Version(System::Diagnostics::FileVersionInfo::GetVersionInfo(toDotNetString(mMoInfo->managedGame()->gameDirectory().filePath("oblivion.exe")))->FileVersion); +} + +System::Version^ ScriptFunctions::OBSEPluginVersion(System::String^ path) +{ + QString pluginPath = mMoInfo->resolvePath(toQString(System::IO::Path::Combine("obse", "plugins", System::IO::Path::ChangeExtension(path, ".dll")))); + if (pluginPath.isEmpty()) + return nullptr; + return gcnew System::Version(System::Diagnostics::FileVersionInfo::GetVersionInfo(toDotNetString(pluginPath))->FileVersion); +} + +System::Collections::Generic::IEnumerable^ ScriptFunctions::GetESPs() +{ + QStringList plugins = mMoInfo->pluginList()->pluginNames(); + System::Collections::Generic::List^ pluginList = gcnew System::Collections::Generic::List(plugins.count()); + for (const auto& pluginName : plugins) + { + auto state = mMoInfo->pluginList()->state(pluginName); + if (state != MOBase::IPluginList::PluginState::STATE_MISSING) + { + OMODFramework::Scripting::ScriptESP plugin; + plugin.Name = toDotNetString(pluginName); + plugin.Active = state == MOBase::IPluginList::PluginState::STATE_ACTIVE; + pluginList->Add(plugin); + } + } + return pluginList; +} + +System::Collections::Generic::IEnumerable^ ScriptFunctions::GetActiveOMODNames() +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. No known OMODs seem to actually use this (which is irritating as this is one of OBMM's most powerful features). +} + +cli::array^ ScriptFunctions::ReadExistingDataFile(System::String^ file) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} + +cli::array^ ScriptFunctions::GetDataFileFromBSA(System::String^ file) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} + +cli::array^ ScriptFunctions::GetDataFileFromBSA(System::String^ bsa, System::String^ file) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} diff --git a/libs/installer_omod/src/implementations/ScriptFunctions.h b/libs/installer_omod/src/implementations/ScriptFunctions.h new file mode 100644 index 0000000..28f9dee --- /dev/null +++ b/libs/installer_omod/src/implementations/ScriptFunctions.h @@ -0,0 +1,122 @@ +#pragma once + +using namespace cli; + +#include + +#include + +#include + +#include "../MessageBoxHelper.h" +#include "../QObject_unique_ptr.h" + +class ScriptFunctionsHelper : public QObject +{ + Q_OBJECT + +public: + ScriptFunctionsHelper(); + + // don't bother with std::forward and decltype(auto) as we know everything is fine being copied + // in fact, as some stuff lives on the managed heap, we can't take a reference anyway + template auto critical(Args... args) { return mMessageBoxHelper->critical(args...); } + template auto information(Args... args) { return mMessageBoxHelper->information(args...); } + template auto question(Args... args) { return mMessageBoxHelper->question(args...); } + template auto warning(Args... args) { return mMessageBoxHelper->warning(args...); } + + std::optional> DialogSelect(QWidget* parent, const QString& title, const QVector& items, + const QVector& descriptions, const QVector& pixmaps, + bool multiSelect); + + QString InputString(QWidget* parentWidget, const QString& title, const QString& initialText); + + void DisplayImage(QWidget* parentWidget, const QString& path, const QString& title); + + void DisplayText(QWidget* parentWidget, const QString& path, const QString& title); + +signals: + void DialogSelectSignal(std::optional>& resultOut, QWidget* parent, const QString& title, const QVector& items, + const QVector& descriptions, const QVector& pixmaps, bool multiSelect); + + void InputStringSignal(QString& textOut, QWidget* parentWidget, const QString& title, const QString& initialText); + + void DisplayImageSignal(QWidget* parentWidget, const QString& path, const QString& title); + + void DisplayTextSignal(QWidget* parentWidget, const QString& path, const QString& title); + +public slots: + void DialogSelectSlot(std::optional>& resultOut, QWidget* parent, const QString& title, const QVector& items, + const QVector& descriptions, const QVector& pixmaps, bool multiSelect); + + void InputStringSlot(QString& textOut, QWidget* parentWidget, const QString& title, const QString& initialText); + + void DisplayImageSlot(QWidget* parentWidget, const QString& path, const QString& title); + + void DisplayTextSlot(QWidget* parentWidget, const QString& path, const QString& title); + +private: + QObject_unique_ptr mMessageBoxHelper; +}; + +ref class ScriptFunctions : OMODFramework::Scripting::IScriptFunctions +{ +public: + ScriptFunctions(QWidget* parentWidget, MOBase::IOrganizer* moInfo); + ~ScriptFunctions(); + !ScriptFunctions(); + + // note: C++/CLI wants virtual for interface implementations, not override + virtual void Warn(System::String^ msg); + + virtual void Message(System::String^ msg); + + virtual void Message(System::String^ msg, System::String^ title); + + virtual System::Collections::Generic::List^ Select(System::Collections::Generic::List^ items, System::String^ title, bool isMultiSelect, System::Collections::Generic::List^ previews, System::Collections::Generic::List^ descriptions); + + virtual System::String^ InputString(System::String^ title, System::String^ initialText); + + virtual int DialogYesNo(System::String^ message); + + virtual int DialogYesNo(System::String^ message, System::String^ title); + + virtual void DisplayImage(System::String^ path, System::String^ title); + + virtual void DisplayText(System::String^ text, System::String^ title); + + virtual void Patch(System::String^ from, System::String^ to); + + virtual System::String^ ReadOblivionINI(System::String^ section, System::String^ name); + + virtual System::String^ ReadRendererInfo(System::String^ name); + + virtual bool DataFileExists(System::String^ path); + + virtual bool HasScriptExtender(); + + virtual bool HasGraphicsExtender(); + + virtual System::Version^ ScriptExtenderVersion(); + + virtual System::Version^ GraphicsExtenderVersion(); + + virtual System::Version^ OblivionVersion(); + + virtual System::Version^ OBSEPluginVersion(System::String^ path); + + virtual System::Collections::Generic::IEnumerable^ GetESPs(); + + virtual System::Collections::Generic::IEnumerable^ GetActiveOMODNames(); + + virtual cli::array^ ReadExistingDataFile(System::String^ file); + + virtual cli::array^ GetDataFileFromBSA(System::String^ file); + + virtual cli::array^ GetDataFileFromBSA(System::String^ bsa, System::String^ file); + +private: + QWidget* mParentWidget; + MOBase::IOrganizer* mMoInfo; + ScriptFunctionsHelper* mHelper; +}; diff --git a/libs/installer_omod/src/installerOmod.cpp b/libs/installer_omod/src/installerOmod.cpp new file mode 100644 index 0000000..3e4c390 --- /dev/null +++ b/libs/installer_omod/src/installerOmod.cpp @@ -0,0 +1,204 @@ +#include "installerOmod.h" + +#include +#include + +#include +#include + +#include "OMODFrameworkWrapper.h" + +InstallerOMOD::InstallerOMOD() : mMoInfo(nullptr), mOmodFrameworkWrapper(nullptr) +{ +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IPlugin + +bool InstallerOMOD::init(MOBase::IOrganizer* moInfo) +{ + mMoInfo = moInfo; + mOmodFrameworkWrapper = std::make_unique(mMoInfo); + mMoInfo->onAboutToRun([this](const QString&) { buildSDPs(); return true; }); + mMoInfo->onFinishedRun([this](const QString&, unsigned int) { clearSDPs(); }); + return true; +} + +QString InstallerOMOD::name() const +{ + return "Omod Installer"; +} + +QString InstallerOMOD::localizedName() const +{ + return tr("Omod Installer"); +} + +QString InstallerOMOD::author() const +{ + return "AnyOldName3 & erril120"; +} + +QString InstallerOMOD::description() const +{ + return tr("Installer for Omod files (including scripted ones)"); +} + +MOBase::VersionInfo InstallerOMOD::version() const +{ + return MOBase::VersionInfo(1, 1, 0, MOBase::VersionInfo::RELEASE_FINAL); +} + +std::vector> InstallerOMOD::requirements() const +{ + return { Requirements::gameDependency("Oblivion") }; +} + +QList InstallerOMOD::settings() const +{ + return {}; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IPluginInstaller + +unsigned int InstallerOMOD::priority() const +{ + // Some other installers have a use_any_file setting and then they'll try and claim OMODs as their own, so we want higher priority than them. + return 500; +} + +bool InstallerOMOD::isManualInstaller() const +{ + return false; +} + +void InstallerOMOD::onInstallationEnd(EInstallResult result, MOBase::IModInterface* newMod) +{ + mOmodFrameworkWrapper->onInstallationEnd(result, newMod); +} + +bool InstallerOMOD::isArchiveSupported(std::shared_ptr tree) const +{ + for (const auto file : *tree) { + // config is the only file guaranteed to be there + if (file->isFile() && file->name() == "config") + return true; + } + return false; +} + +void InstallerOMOD::setParentWidget(QWidget* parent) +{ + IPluginInstaller::setParentWidget(parent); + mOmodFrameworkWrapper->setParentWidget(parent); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IPluginInstallerCustom + +bool InstallerOMOD::isArchiveSupported(const QString& archiveName) const +{ + return archiveName.endsWith(".omod", Qt::CaseInsensitive); +} + +std::set InstallerOMOD::supportedExtensions() const +{ + return { "omod" }; +} + +InstallerOMOD::EInstallResult InstallerOMOD::install(MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) +{ + return mOmodFrameworkWrapper->installInAnotherThread(modName, gameName, archiveName, version, nexusID); +} + +MappingType InstallerOMOD::mappings() const +{ + MOBase::log::debug("Mapping virtual SDPs"); + + MappingType mappings; + + for (const auto& [realSDPPath, virtualSDP] : mVirtualSDPs) + mappings.push_back({ virtualSDP->fileName(), realSDPPath, false, false }); + + return mappings; +} + +void InstallerOMOD::buildSDPs() +{ + QStringList directories = mMoInfo->listDirectories("Shaders/OMOD"); + directories = directories.filter(QRegularExpression("\\d+")); + + for (const auto& directory : directories) + { + QString packagePath = mMoInfo->resolvePath(QString("Shaders/shaderpackage%1.sdp").arg(directory.toInt(), 3, 10, QChar('0'))); + if (packagePath.isEmpty()) + continue; + + QMap shaders; + { + QFile packageFile(packagePath); + packageFile.open(QIODevice::ReadOnly); + QDataStream basePackageStream(&packageFile); + basePackageStream.setByteOrder(QDataStream::LittleEndian); + quint32 magic; + basePackageStream >> magic; + if (magic != 100) + { + MOBase::log::debug("SDP magic number mismatch in {}, got {}, but expected 100", packagePath, magic); + continue; + } + quint32 shaderCount, totalSize; + basePackageStream >> shaderCount >> totalSize; + for (quint32 i = 0; i < shaderCount && !basePackageStream.atEnd(); ++i) + { + QByteArray shaderName(256, '\0'); + basePackageStream.readRawData(shaderName.data(), 256); + quint32 shaderSize; + basePackageStream >> shaderSize; + QByteArray shaderBytecode(shaderSize, '\0'); + basePackageStream.readRawData(shaderBytecode.data(), shaderSize); + shaders[shaderName] = shaderBytecode; + } + } + + QStringList shaderFiles = mMoInfo->findFiles("Shaders/OMOD/" + directory, { "*.vso", "*.pso" }); + for (const auto& shaderFile : shaderFiles) + { + QFileInfo info(shaderFile); + QByteArray shaderName = (info.baseName().toUpper() + "." + info.suffix().toLower()).toLatin1(); + QFile file(shaderFile); + file.open(QIODevice::ReadOnly); + shaderName = shaderName.leftJustified(256, '\0', true); + shaders[shaderName] = file.readAll(); + MOBase::log::debug("Replacement for {} was {} bytes long", shaderFile, shaders[shaderName].size()); + } + + std::unique_ptr outputFile = std::make_unique(); + outputFile->open(); + QDataStream packageStream(outputFile.get()); + packageStream.setByteOrder(QDataStream::LittleEndian); + packageStream << quint32(100) << quint32(shaders.count()); + + quint32 totalSize = 0; + for (const auto& shader : shaders) + totalSize += 256 + 4 + shader.size(); + packageStream << totalSize; + + for (auto itr = shaders.cbegin(); itr != shaders.cend(); ++itr) + { + packageStream.writeRawData(itr.key().constData(), 256); + packageStream << quint32(itr.value().size()); + packageStream.writeRawData(itr.value().constData(), itr.value().size()); + } + + outputFile->close(); + + mVirtualSDPs.emplace(mMoInfo->managedGame()->dataDirectory().absoluteFilePath(QString("Shaders/shaderpackage%1.sdp").arg(directory.toInt(), 3, 10, QChar('0'))), std::move(outputFile)); + } +} + +void InstallerOMOD::clearSDPs() +{ + mVirtualSDPs.clear(); +} diff --git a/libs/installer_omod/src/installerOmod.h b/libs/installer_omod/src/installerOmod.h new file mode 100644 index 0000000..a122355 --- /dev/null +++ b/libs/installer_omod/src/installerOmod.h @@ -0,0 +1,75 @@ +#ifndef INSTALLEROMOD_H +#define INSTALLEROMOD_H + +#include + +#include +#include + +#include "OMODFrameworkWrapper.h" + +class InstallerOMOD : public MOBase::IPluginInstallerCustom, public MOBase::IPluginFileMapper +{ + Q_OBJECT; + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerCustom MOBase::IPluginFileMapper); +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + // We should probably buy a domain. + Q_PLUGIN_METADATA(IID "org.AnyOldName3.InstallerOmod"); +#endif + +public: + InstallerOMOD(); + + // IPlugin + bool init(MOBase::IOrganizer* moInfo) override; + + QString name() const override; + + QString localizedName() const override; + + QString author() const override; + + QString description() const override; + + MOBase::VersionInfo version() const override; + + std::vector> requirements() const override; + + QList settings() const override; + + // IPluginInstaller + + unsigned int priority() const override; + + bool isManualInstaller() const override; + + void onInstallationEnd(EInstallResult result, MOBase::IModInterface* newMod) override; + + bool isArchiveSupported(std::shared_ptr tree) const override; + + void setParentWidget(QWidget* parent) override; + + // IPluginInstallerCustom + + bool isArchiveSupported(const QString& archiveName) const override; + + std::set supportedExtensions() const override; + + EInstallResult install(MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) override; + + // IPluginFileMapper + + MappingType mappings() const override; + +protected: + void buildSDPs(); + + void clearSDPs(); + +private: + MOBase::IOrganizer* mMoInfo; + std::unique_ptr mOmodFrameworkWrapper; + std::map> mVirtualSDPs; +}; + +#endif // !INSTALLEROMOD_H diff --git a/libs/installer_omod/src/installer_omod_en.ts b/libs/installer_omod/src/installer_omod_en.ts new file mode 100644 index 0000000..a092611 --- /dev/null +++ b/libs/installer_omod/src/installer_omod_en.ts @@ -0,0 +1,159 @@ + + + + + InstallerOMOD + + + Omod Installer + + + + + Installer for Omod files (including scripted ones) + + + + + NameDialog + + + Pick mod name + + + + + Name + + + + + + Manual install is unavailable for OMODs. + + + + + Manual + + + + + OK + + + + + Cancel + + + + + OMODFrameworkWrapper + + + %1 wants to change [%2] %3 from "%4" to "%5" + %1 is the mod name [%2] is the ini section name. %3 is the ini setting name. %4 is the value already in Oblivion.ini. %5 is the value the mod wants to set. + + + + + %1 wants to set [%2] %3 to "%4" + %1 is the mod name [%2] is the ini section name. %3 is the ini setting name. %5 is the value the mod wants to set. + + + + + Update INI? + + + + + %1 has data for %2, but Mod Organizer 2 doesn't know what to do with it yet. Please report this to the Mod Organizer 2 development team (ideally by sending us your interface log) as we didn't find any OMODs that actually did this, and we need to know that they exist. + %1 is the mod name %2 is the name of a field in the OMOD's return data Hopefully this message will never be seen by anyone, but if it is, they need to know to tell the Mod Organizer 2 dev team. + + + + + Mod Organizer 2 can't completely install this OMOD. + + + + + Activate mod? + + + + + %1 contains the OMOD %2. OMODs may have post-installation actions like activating ESPs. Would you like to enable the mod so this can happen now? + %1 is the left-pane mod name. %2 is the name from the metadata of an OMOD. + + + + + %1 wants to activate %2. Do you want to do so? + %1 is the mod name. %2 is the plugin name. + + + + + + Activate plugin? + + + + + OMOD wants to activate missing plugin + + + + + An OMOD wants to activate a missing plugin. This shouldn't be possible. Please report this to a MO2 developer. + + + + + %1 installed %2, but doesn't activate it by default. Do you want to activate it anyway? + %1 is the mod name. %2 is the plugin name. + + + + + OMOD claimed to install missing plugin + + + + + An OMOD has activation settings for a missing plugin. This shouldn't be possible. Please report this to a MO2 developer. + + + + + Register BSAs + + + + + %1 wants to register the following BSA archives, but Mod Organizer 2 can't do that yet due to technical limitations:<ul><li>%2</li></ul>For now, your options include adding the BSA names to <code>sResourceArchiveList</code> in the game INI, creating a dummy ESP with the same name, or extracting the BSA, all of which have drawbacks. + %1 is the OMOD name <ul><li>%2</li></ul> becomes a list of BSA files + + + + + Display Readme? + + + + + The Readme may explain installation options. Display it?<br>It will remain visible until you close it. + <br> is a line break. Translators can remove it if it makes things clearer. + + + + + %1 Readme + %1 is the mod name + + + + diff --git a/libs/installer_omod/src/interop/QtDotNetConverters.cpp b/libs/installer_omod/src/interop/QtDotNetConverters.cpp new file mode 100644 index 0000000..430e8e5 --- /dev/null +++ b/libs/installer_omod/src/interop/QtDotNetConverters.cpp @@ -0,0 +1,17 @@ +#include "QtDotNetConverters.h" + +#include + +#include + +QString toQString(System::String^ string) +{ + if (string) + return QString::fromStdWString(msclr::interop::marshal_as(string)); + return QString(); +} + +System::String^ toDotNetString(const QString& qString) +{ + return qString.isNull() ? nullptr : msclr::interop::marshal_as(qString.toStdWString()); +} diff --git a/libs/installer_omod/src/interop/QtDotNetConverters.h b/libs/installer_omod/src/interop/QtDotNetConverters.h new file mode 100644 index 0000000..6cca053 --- /dev/null +++ b/libs/installer_omod/src/interop/QtDotNetConverters.h @@ -0,0 +1,9 @@ +#pragma once + +using namespace cli; + +#include + +QString toQString(System::String^ string); + +System::String^ toDotNetString(const QString& qString); diff --git a/libs/installer_omod/src/interop/StdDotNetConverters.cpp b/libs/installer_omod/src/interop/StdDotNetConverters.cpp new file mode 100644 index 0000000..be7497b --- /dev/null +++ b/libs/installer_omod/src/interop/StdDotNetConverters.cpp @@ -0,0 +1,35 @@ +#include "stdDotNetConverters.h" + +#include + +#include + +std::wstring toWString(System::String^ string) +{ + if (string) + return msclr::interop::marshal_as(string); + return L"null .NET string"; +} + +System::String^ toDotNetString(const std::wstring& wString) +{ + return msclr::interop::marshal_as(wString); +} + +std::string toUTF8String(System::String^ string) +{ + if (string) + { + array^ utf8 = System::Text::Encoding::UTF8->GetBytes(string); + std::string stdstring; + stdstring.resize(utf8->Length); + System::Runtime::InteropServices::Marshal::Copy(utf8, 0, System::IntPtr(stdstring.data()), utf8->Length); + return stdstring; + } + return "null .NET string"; +} + +std::exception toStdException(System::Exception^ exception) +{ + return std::runtime_error("Unhanded .NET Exception: " + toUTF8String(exception->ToString())); +} diff --git a/libs/installer_omod/src/interop/StdDotNetConverters.h b/libs/installer_omod/src/interop/StdDotNetConverters.h new file mode 100644 index 0000000..04c2ada --- /dev/null +++ b/libs/installer_omod/src/interop/StdDotNetConverters.h @@ -0,0 +1,13 @@ +#pragma once + +using namespace cli; + +#include + +std::wstring toWString(System::String^ string); + +std::string toUTF8String(System::String^ string); + +System::String^ toDotNetString(const std::wstring& wString); + +std::exception toStdException(System::Exception^ exception); diff --git a/libs/installer_omod/src/newstuff/namedialog.cpp b/libs/installer_omod/src/newstuff/namedialog.cpp new file mode 100644 index 0000000..4dd44d6 --- /dev/null +++ b/libs/installer_omod/src/newstuff/namedialog.cpp @@ -0,0 +1,37 @@ +#include "namedialog.h" + +#include + +NameDialog::NameDialog(const MOBase::GuessedValue& suggestedNames, QWidget* parent) + : QDialog(parent), ui(), mName(suggestedNames) +{ + ui.setupUi(this); + + for (const auto& name : suggestedNames.variants()) + ui.nameCombo->addItem(name); + + ui.nameCombo->setCurrentIndex(ui.nameCombo->findText(suggestedNames)); + + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + ui.nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); +} + +QString NameDialog::getName() const +{ + return mName; +} + +void NameDialog::on_okBtn_clicked() +{ + accept(); +} + +void NameDialog::on_cancelBtn_clicked() +{ + reject(); +} + +void NameDialog::on_nameCombo_currentTextChanged(const QString& text) +{ + mName = text; +} diff --git a/libs/installer_omod/src/newstuff/namedialog.h b/libs/installer_omod/src/newstuff/namedialog.h new file mode 100644 index 0000000..72e62eb --- /dev/null +++ b/libs/installer_omod/src/newstuff/namedialog.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include + +#include "ui_namedialog.h" + +class NameDialog : public QDialog +{ + Q_OBJECT + +public: + NameDialog(const MOBase::GuessedValue& suggestedNames, QWidget* parent = nullptr); + + QString getName() const; + +private slots: + void on_okBtn_clicked(); + + void on_cancelBtn_clicked(); + + void on_nameCombo_currentTextChanged(const QString& text); + +private: + Ui::NameDialog ui; + QString mName; +}; diff --git a/libs/installer_omod/src/newstuff/namedialog.ui b/libs/installer_omod/src/newstuff/namedialog.ui new file mode 100644 index 0000000..5727d1a --- /dev/null +++ b/libs/installer_omod/src/newstuff/namedialog.ui @@ -0,0 +1,89 @@ + + + NameDialog + + + + 0 + 0 + 400 + 83 + + + + Pick mod name + + + + + + + + Name + + + + + + + true + + + + + + + + + + + false + + + Manual install is unavailable for OMODs. + + + Manual install is unavailable for OMODs. + + + Manual + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + OK + + + true + + + + + + + Cancel + + + + + + + + + + diff --git a/libs/installer_omod/src/newstuff/rtfPopup.cpp b/libs/installer_omod/src/newstuff/rtfPopup.cpp new file mode 100644 index 0000000..476f078 --- /dev/null +++ b/libs/installer_omod/src/newstuff/rtfPopup.cpp @@ -0,0 +1,33 @@ +#include "rtfPopup.h" + +#include +#include +#include +#include +#include + +#include "../interop/QtDotNetConverters.h" + +RtfPopup::RtfPopup(System::String^ rtfText, QWidget* parent, Qt::WindowFlags f) : QDialog(parent, f) +{ + QString text = rtfText->StartsWith("{\\rtf") ? toQString(RtfPipe::Rtf::ToHtml(rtfText)) : Qt::convertFromPlainText(toQString(rtfText), Qt::WhiteSpaceNormal); + QRegularExpression urlFinder(R"REGEX((?\1)"); + + QLayout* layout = new QGridLayout(this); + setLayout(layout); + QScrollArea* scrollArea = new QScrollArea(this); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + layout->addWidget(scrollArea); + + QLabel* label = new QLabel(text, scrollArea); + label->setWordWrap(true); + label->setTextFormat(Qt::RichText); + label->setOpenExternalLinks(true); + label->setTextInteractionFlags(Qt::TextBrowserInteraction); + scrollArea->setWidget(label); + scrollArea->setWidgetResizable(true); + + setSizeGripEnabled(true); +} diff --git a/libs/installer_omod/src/newstuff/rtfPopup.h b/libs/installer_omod/src/newstuff/rtfPopup.h new file mode 100644 index 0000000..17ca10c --- /dev/null +++ b/libs/installer_omod/src/newstuff/rtfPopup.h @@ -0,0 +1,11 @@ +using namespace cli; + +#include + +class RtfPopup : public QDialog +{ + Q_OBJECT + +public: + RtfPopup(System::String^ rtfText, QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); +}; diff --git a/libs/installer_omod/src/oldstuff/DialogSelect.cpp b/libs/installer_omod/src/oldstuff/DialogSelect.cpp new file mode 100644 index 0000000..9d387a8 --- /dev/null +++ b/libs/installer_omod/src/oldstuff/DialogSelect.cpp @@ -0,0 +1,301 @@ +#include "DialogSelect.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MIT-licencedCodeToDoStuff/checkboxwordwrap.h" + +// there is no hover signal, the only way to know is to override enterEvent() +// +template +class HoverableWidget : public T +{ +public: + std::function onHover; + + HoverableWidget(Args... args, std::function h) + : T(args...), onHover(std::move(h)) + { + } + +protected: void enterEvent(QEnterEvent*) override + { + onHover(); + } +}; + + +FixedAspectRatioImageLabel::FixedAspectRatioImageLabel(QWidget* parent) : QLabel(parent) +{ +} + +void FixedAspectRatioImageLabel::setUnscaledPixmap(const QPixmap& pixmap) +{ + mUnscaledPixmap = pixmap; + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + rescalePixmap(size()); + + copySizeLabel.setPixmap(pixmap); +} + +const QPixmap& FixedAspectRatioImageLabel::unscaledPixmap() const +{ + return mUnscaledPixmap; +} + +QSize FixedAspectRatioImageLabel::sizeHint() const +{ + if (mUnscaledPixmap.isNull()) + { + return QLabel::sizeHint(); + } + else + { + // maybe should add frame border + return mUnscaledPixmap.size(); + //return copySizeLabel.sizeHint(); + } +} + +bool FixedAspectRatioImageLabel::hasHeightForWidth() const +{ + return true; +} + +int FixedAspectRatioImageLabel::heightForWidth(int width) const +{ + // this ignores the difference between size and contentsRect size + return mUnscaledPixmap.height() * width / (double)mUnscaledPixmap.width(); +} + +int FixedAspectRatioImageLabel::widthForHeight(int height) const +{ + return mUnscaledPixmap.width() * height / (double)mUnscaledPixmap.height(); +} + +void FixedAspectRatioImageLabel::resizeEvent(QResizeEvent* resizeEvent) +{ + QLabel::resizeEvent(resizeEvent); + rescalePixmap(contentsRect().size()); +} + +void FixedAspectRatioImageLabel::rescalePixmap(const QSize& size) +{ + setPixmap(mUnscaledPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation)); +} + + +std::optional> DialogSelect( + QWidget* parent, const QString& title, const QVector& items, + const QVector& descriptions, const QVector& pixmaps, + bool multiSelect) +{ + QDialog d(parent); + d.setWindowTitle(title); + + auto* mainLayout = new QVBoxLayout(&d); + d.setLayout(mainLayout); + + auto* splitter = new QSplitter(&d); + mainLayout->addWidget(splitter); + + // button box + auto* buttons = new QDialogButtonBox( + QDialogButtonBox::Ok|QDialogButtonBox::Cancel, &d); + + /* QObject::connect: signal not found in QDialogButtonBox + even though this worked with my initial attempt. + Holt had the same issue: https://stackoverflow.com/questions/61879664/qobjectconnect-not-working-signal-not-found-with-function-syntax + The new syntax is much better, but as this code wants replacing, that's not a hill I'm going to die on. + QObject::connect(buttons, &QDialogButtonBox::accepted, [&] { + d.accept(); + }); + + QObject::connect(buttons, &QDialogButtonBox::rejected, [&] { + d.reject(); + }); + */ + QObject::connect(buttons, SIGNAL(accepted()), &d, SLOT(accept())); + QObject::connect(buttons, SIGNAL(rejected()), &d, SLOT(reject())); + + mainLayout->addWidget(buttons); + + + // left panel + auto* left = new QWidget(splitter); + auto* leftLayout = new QVBoxLayout(left); + leftLayout->setContentsMargins(0, 0, 0, 0); + + auto* stack = new QStackedWidget(left); + leftLayout->addWidget(stack); + + // don't put descriptions of pixmaps at all if there aren't any + const bool hasDescriptions = !descriptions.empty(); + const bool hasPixmaps = !pixmaps.empty(); + + // for each description/item + for (int i=0; i < std::max(descriptions.size(), pixmaps.size()); ++i) { + auto* panel = new QWidget(left); + auto* panelLayout = new QVBoxLayout(panel); + //panelLayout->setContentsMargins(0, 0, 0, 0); + + if (hasPixmaps) { + auto* pixmapLabel = new FixedAspectRatioImageLabel(panel); + + pixmapLabel->setFrameStyle(QFrame::StyledPanel); + pixmapLabel->setLineWidth(1); + + // make it resizable + //pixmapLabel->setMinimumSize(1, 150); + //pixmapLabel->setScaledContents(true); + //pixmapLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + if (i < pixmaps.size()) { + // this item has a pixmap + // Use QImageReader as (despite what the documentation says) QPixmap uses the extension, rather than the file header, to determine the format. + QImageReader reader(pixmaps[i]); + reader.setDecideFormatFromContent(true); + QImage image = reader.read(); + if (image.isNull()) { + pixmapLabel->setText(QString("failed to load '%1': %2 (code %3)").arg(pixmaps[i], reader.errorString(), QString::number(reader.error()))); + } else { + QPixmap pixmap = QPixmap::fromImage(image); + pixmapLabel->setUnscaledPixmap(std::move(pixmap)); + } + } + + panelLayout->addWidget(pixmapLabel); + } + + if (hasDescriptions) + { + // description under pixmap (if any) + auto* description = new QPlainTextEdit(descriptions[i], panel); + + // this puts the description on top if there's no pixmap + //description->setAlignment(Qt::AlignLeft|Qt::AlignTop); + + //description->setWordWrap(true); + + panelLayout->addWidget(description); + } + + // if the pixmap is first, it'll take as much space as it can; if the + // description is first, the AlignTop makes it work anyways + panelLayout->setStretch(0, 1); + + stack->addWidget(panel); + } + + + // right panel + auto* right = new QWidget(&d); + auto* rightOuterLayout = new QVBoxLayout(right); + rightOuterLayout->setContentsMargins(0, 0, 0, 0); + + // title + auto* titleLabel = new QLabel(title, right); + titleLabel->setWordWrap(true); + rightOuterLayout->addWidget(titleLabel); + + QWidget* rightInner; + QBoxLayout* rightInnerLayout; + + // Doing this unconditionally breaks HGEC lower body choices. + // Somehow the fixed aspect ratio image label forces the scroll area to be resized below its preferred size. + // Because the word wrapped radio buttons aren't good at telling Qt their size hint is only minimal for the current width, + // the layout ends up with their preferred width as its minimum and won't let itself be shrunk smaller than that even though you can't see the right hand side. + // This means that as they're in a widget with enough space, the text isn't wrapped. + // The exact same behaviour happens with non-wrappable radio buttons, but it's more expected. + // Making the fixed aspect ratio image label play nicely with being shrunk will probably fix this. + // It only seems to be MEAT that actually needs a scrollbar, though, and that looks fine, so fixing this mess is left as an exercise for the reader. + if (items.size() >= 10) + { + QScrollArea* rightScrollArea = new QScrollArea(right); + rightOuterLayout->addWidget(rightScrollArea); + rightScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + rightScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + + rightInner = new QWidget(right); + rightScrollArea->setWidget(rightInner); + rightScrollArea->setWidgetResizable(true); + + rightInnerLayout = new QVBoxLayout(rightInner); + } + else + { + rightInner = right; + rightInnerLayout = rightOuterLayout; + } + + // callback when hovering + auto onHover = [&](int i) { + stack->setCurrentIndex(i); + }; + + + // remember buttons to see which were checked + QVector itemButtons; + + // for each item + for (int i=0; i(labelText, rightInner, [=]{ onHover(i); }); + } else { + w = new HoverableWidget(labelText, rightInner, [=]{ onHover(i); }); + checked |= i == 0; + } + + rightInnerLayout->addWidget(w); + itemButtons.push_back(w); + w->setChecked(checked); + } + + // push all the items to the top + rightInnerLayout->addStretch(1); + + splitter->addWidget(left); + splitter->addWidget(right); + + // decent initial size + d.resize(800, 500); + + if (d.exec() != QDialog::Accepted) { + return std::nullopt; + } + + + // go through every button, it doesn't matter whether they're checkboxes or + // radio buttons + QVector selection; + for (int i=0; iisChecked()) { + selection.push_back(i); + } + } + + return selection; +} diff --git a/libs/installer_omod/src/oldstuff/DialogSelect.h b/libs/installer_omod/src/oldstuff/DialogSelect.h new file mode 100644 index 0000000..0e0f1bb --- /dev/null +++ b/libs/installer_omod/src/oldstuff/DialogSelect.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include +#include + +std::optional> DialogSelect(QWidget* parent, const QString& title, const QVector& items, + const QVector& descriptions, const QVector& pixmaps, + bool multiSelect); + + +// For some reason, this can be resized bigger but not smaller. +class FixedAspectRatioImageLabel : public QLabel +{ + Q_OBJECT + + Q_PROPERTY(QPixmap unscaledPixmap READ unscaledPixmap WRITE setUnscaledPixmap) + +public: + FixedAspectRatioImageLabel() = default; + + FixedAspectRatioImageLabel(QWidget* parent); + + void setUnscaledPixmap(const QPixmap& pixmap); + + const QPixmap& unscaledPixmap() const; + + QSize sizeHint() const override; + + bool hasHeightForWidth() const override; + + int heightForWidth(int width) const override; + + int widthForHeight(int height) const; + +protected: + void resizeEvent(QResizeEvent* resizeEvent) override; + +private: + void rescalePixmap(const QSize& size); + + QPixmap mUnscaledPixmap; + QLabel copySizeLabel; +}; diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE new file mode 100644 index 0000000..e2874c2 --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) unknown thibdev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp new file mode 100644 index 0000000..d686c58 --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp @@ -0,0 +1,165 @@ +#include "checkboxwordwrap.h" + +#include +#include + +CheckBoxWordWrap::CheckBoxWordWrap(QWidget *parent) + : QCheckBox (parent) + , m_hMainLayout(new QHBoxLayout(this)) + , m_label(new ClickableLabel(this)) +{ + init(); +} + +CheckBoxWordWrap::CheckBoxWordWrap(const QString &text, QWidget *parent) + : QCheckBox (parent) + , m_hMainLayout(new QHBoxLayout(this)) + , m_label(new ClickableLabel(text, this)) +{ + init(); +} + +CheckBoxWordWrap::~CheckBoxWordWrap() +{ + delete m_label; + delete m_hMainLayout; +} + +bool CheckBoxWordWrap::isWordWrap() const +{ + return m_label->wordWrap(); +} + +void CheckBoxWordWrap::setWordWrap(bool wordwrap) +{ + m_label->setWordWrap(wordwrap); +} + +QString CheckBoxWordWrap::text() const +{ + return m_label->text(); +} + +void CheckBoxWordWrap::setText(const QString &text) +{ + m_label->setText(text); +} + +QSize CheckBoxWordWrap::sizeHint() const +{ + QFontMetrics fm(m_label->font()); + QRect r = m_label->rect(); + r.setLeft(r.left()+m_label->indent()+separation); + QRect bRect = fm.boundingRect(r, int(Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap), m_label->text()); + QSize ret = QSize(QWidget::sizeHint().width(), bRect.height()); + return ret; +} + +void CheckBoxWordWrap::labelIsClicked() +{ + setChecked(!isChecked()); +} + +void CheckBoxWordWrap::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + updateGeometry(); +} + +void CheckBoxWordWrap::init() +{ + setLayout(m_hMainLayout); + QStyleOptionButton opt; + initStyleOption(&opt); + int indicatorW = style()->pixelMetric(QStyle::PixelMetric::PM_IndicatorWidth, &opt, this); + // Useless in our case, we only need the indicator width + //int indicatorH = style()->pixelMetric(QStyle::PixelMetric::PM_IndicatorHeight, &opt, this); + m_hMainLayout->setContentsMargins(0, 0, 0, 0); + m_hMainLayout->addWidget(m_label); + m_label->setIndent(indicatorW+separation); + m_label->setWordWrap(true); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + connect(m_label, SIGNAL(clicked()), this, SLOT(labelIsClicked())); +} + + +RadioButtonWordWrap::RadioButtonWordWrap(QWidget* parent) + : QRadioButton(parent) + , m_hMainLayout(new QHBoxLayout(this)) + , m_label(new ClickableLabel(this)) +{ + init(); +} + +RadioButtonWordWrap::RadioButtonWordWrap(const QString& text, QWidget* parent) + : QRadioButton(parent) + , m_hMainLayout(new QHBoxLayout(this)) + , m_label(new ClickableLabel(text, this)) +{ + init(); +} + +RadioButtonWordWrap::~RadioButtonWordWrap() +{ + delete m_label; + delete m_hMainLayout; +} + +bool RadioButtonWordWrap::isWordWrap() const +{ + return m_label->wordWrap(); +} + +void RadioButtonWordWrap::setWordWrap(bool wordwrap) +{ + m_label->setWordWrap(wordwrap); +} + +QString RadioButtonWordWrap::text() const +{ + return m_label->text(); +} + +void RadioButtonWordWrap::setText(const QString& text) +{ + m_label->setText(text); +} + +QSize RadioButtonWordWrap::sizeHint() const +{ + QFontMetrics fm(m_label->font()); + QRect r = m_label->rect(); + r.setLeft(r.left() + m_label->indent() + separation); + QRect bRect = fm.boundingRect(r, int(Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap), m_label->text()); + QSize ret = QSize(QWidget::sizeHint().width(), bRect.height()); + return ret; +} + +void RadioButtonWordWrap::labelIsClicked() +{ + setChecked(!isChecked()); +} + +void RadioButtonWordWrap::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + updateGeometry(); +} + +void RadioButtonWordWrap::init() +{ + setLayout(m_hMainLayout); + QStyleOptionButton opt; + initStyleOption(&opt); + int indicatorW = style()->pixelMetric(QStyle::PixelMetric::PM_IndicatorWidth, &opt, this); + // Useless in our case, we only need the indicator width + //int indicatorH = style()->pixelMetric(QStyle::PixelMetric::PM_IndicatorHeight, &opt, this); + m_hMainLayout->setContentsMargins(0, 0, 0, 0); + m_hMainLayout->addWidget(m_label); + m_label->setIndent(indicatorW + separation); + m_label->setWordWrap(true); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + connect(m_label, SIGNAL(clicked()), this, SLOT(labelIsClicked())); +} diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.h b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.h new file mode 100644 index 0000000..dc4d30c --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.h @@ -0,0 +1,75 @@ +#ifndef CHECKBOXWORDWRAP_H +#define CHECKBOXWORDWRAP_H + +#include +#include +#include + +#include "clickablelabel.h" + +/** + * @author thibdev + */ +class CheckBoxWordWrap : public QCheckBox +{ + Q_OBJECT + + Q_PROPERTY(bool wordwrap READ isWordWrap WRITE setWordWrap) + Q_PROPERTY(QString text READ text WRITE setText) + +public: + CheckBoxWordWrap(QWidget *parent = Q_NULLPTR); + CheckBoxWordWrap(const QString &text, QWidget *parent = Q_NULLPTR); + ~CheckBoxWordWrap(); + bool isWordWrap() const; + void setWordWrap(bool wordwrap); + QString text() const; + void setText(const QString &text); + QSize sizeHint() const override; + +private slots: + void labelIsClicked(); + +protected: + void resizeEvent(QResizeEvent *event) override; + +private: + void init(); + const int separation = 5; + QHBoxLayout *m_hMainLayout; + ClickableLabel *m_label; + +}; + +class RadioButtonWordWrap : public QRadioButton +{ + Q_OBJECT + + Q_PROPERTY(bool wordwrap READ isWordWrap WRITE setWordWrap) + Q_PROPERTY(QString text READ text WRITE setText) + +public: + RadioButtonWordWrap(QWidget* parent = Q_NULLPTR); + RadioButtonWordWrap(const QString& text, QWidget* parent = Q_NULLPTR); + ~RadioButtonWordWrap(); + bool isWordWrap() const; + void setWordWrap(bool wordwrap); + QString text() const; + void setText(const QString& text); + QSize sizeHint() const override; + +private slots: + void labelIsClicked(); + +protected: + void resizeEvent(QResizeEvent* event) override; + +private: + void init(); + const int separation = 5; + QHBoxLayout* m_hMainLayout; + ClickableLabel* m_label; + +}; + +#endif diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.cpp b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.cpp new file mode 100644 index 0000000..6f8d47f --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.cpp @@ -0,0 +1,24 @@ +#include "clickablelabel.h" + +ClickableLabel::ClickableLabel(QWidget *parent, Qt::WindowFlags f) + : QLabel(parent, f) +{ + +} + +ClickableLabel::ClickableLabel(const QString &text, QWidget *parent, Qt::WindowFlags f) + : QLabel(text, parent, f) +{ + +} + +ClickableLabel::~ClickableLabel() +{ + +} + +void ClickableLabel::mouseReleaseEvent(QMouseEvent *event) +{ + Q_UNUSED(event); + emit clicked(); +} diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.h b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.h new file mode 100644 index 0000000..c3ee22b --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.h @@ -0,0 +1,27 @@ +#ifndef CLICKABLELABEL_H +#define CLICKABLELABEL_H + +#include +#include + +/** + * @brief The ClickableLabel class + * https://wiki.qt.io/Clickable_QLabel + */ +class ClickableLabel : public QLabel +{ + Q_OBJECT +public: + explicit ClickableLabel(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); + explicit ClickableLabel(const QString &text, QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); + ~ClickableLabel() override; + +signals: + void clicked(); + +protected: + void mouseReleaseEvent(QMouseEvent *event) override; + +}; + +#endif // CLICKABLELABEL_H diff --git a/libs/installer_omod/src/oldstuff/folder.md b/libs/installer_omod/src/oldstuff/folder.md new file mode 100644 index 0000000..3b5009a --- /dev/null +++ b/libs/installer_omod/src/oldstuff/folder.md @@ -0,0 +1 @@ +This is stuff I'm just lifting from my original attempt at an OMOD installer. It's probably terrible and I'm barely going to fix it. I expect and want other MO2 devs to rework/replace it with something better. diff --git a/libs/installer_omod/vcpkg.json b/libs/installer_omod/vcpkg.json new file mode 100644 index 0000000..28051ad --- /dev/null +++ b/libs/installer_omod/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673" + } + } +} diff --git a/libs/installer_quick/.clang-format b/libs/installer_quick/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/installer_quick/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/installer_quick/.git-blame-ignore-revs b/libs/installer_quick/.git-blame-ignore-revs new file mode 100644 index 0000000..c32a336 --- /dev/null +++ b/libs/installer_quick/.git-blame-ignore-revs @@ -0,0 +1 @@ +4b30cfb8a268d8c0472b148943dd588114916182 diff --git a/libs/installer_quick/.gitattributes b/libs/installer_quick/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/installer_quick/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/installer_quick/.github/workflows/build.yml b/libs/installer_quick/.github/workflows/build.yml new file mode 100644 index 0000000..4f109fe --- /dev/null +++ b/libs/installer_quick/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build Installer Quick + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Installer Quick + id: build-installer-quick + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase diff --git a/libs/installer_quick/.github/workflows/linting.yml b/libs/installer_quick/.github/workflows/linting.yml new file mode 100644 index 0000000..6b4d6c4 --- /dev/null +++ b/libs/installer_quick/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Installer Quick Plugin + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/installer_quick/.gitignore b/libs/installer_quick/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/installer_quick/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/installer_quick/.pre-commit-config.yaml b/libs/installer_quick/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/installer_quick/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/installer_quick/CMakeLists.txt b/libs/installer_quick/CMakeLists.txt new file mode 100644 index 0000000..2d40abe --- /dev/null +++ b/libs/installer_quick/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.16) + +project(installer_quick) +add_subdirectory(src) diff --git a/libs/installer_quick/CMakePresets.json b/libs/installer_quick/CMakePresets.json new file mode 100644 index 0000000..5a63c77 --- /dev/null +++ b/libs/installer_quick/CMakePresets.json @@ -0,0 +1,67 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/installer_quick/src/CMakeLists.txt b/libs/installer_quick/src/CMakeLists.txt new file mode 100644 index 0000000..050ddf0 --- /dev/null +++ b/libs/installer_quick/src/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB installer_quick_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(installer_quick SHARED ${installer_quick_SOURCES}) +mo2_configure_plugin(installer_quick NO_SOURCES WARNINGS 4) +target_link_libraries(installer_quick PRIVATE mo2::uibase) +mo2_install_plugin(installer_quick) diff --git a/libs/installer_quick/src/installer_quick_en.ts b/libs/installer_quick/src/installer_quick_en.ts new file mode 100644 index 0000000..f5aa081 --- /dev/null +++ b/libs/installer_quick/src/installer_quick_en.ts @@ -0,0 +1,51 @@ + + + + + InstallerQuick + + + Simple Installer + + + + + Installer for very simple archives + + + + + SimpleInstallDialog + + + Quick Install + + + + + Name + + + + + + Opens a Dialog that allows custom modifications. + + + + + Manual + + + + + OK + + + + + Cancel + + + + diff --git a/libs/installer_quick/src/installerquick.cpp b/libs/installer_quick/src/installerquick.cpp new file mode 100644 index 0000000..20b4b76 --- /dev/null +++ b/libs/installer_quick/src/installerquick.cpp @@ -0,0 +1,176 @@ +#include "installerquick.h" + +#include +#include + +#include +#include +#include + +#include "simpleinstalldialog.h" + +using namespace MOBase; + +InstallerQuick::InstallerQuick() : m_MOInfo(nullptr) {} + +bool InstallerQuick::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + // Note: Cannot retrieve the checker here because the game might + // not be initialized yet. + return true; +} + +QString InstallerQuick::name() const +{ + return "Simple Installer"; +} + +QString InstallerQuick::localizedName() const +{ + return tr("Simple Installer"); +} + +QString InstallerQuick::author() const +{ + return "Tannin"; +} + +QString InstallerQuick::description() const +{ + return tr("Installer for very simple archives"); +} + +VersionInfo InstallerQuick::version() const +{ + return VersionInfo(1, 3, 0, VersionInfo::RELEASE_FINAL); +} + +QList InstallerQuick::settings() const +{ + return {PluginSetting("silent", + "simple plugins will be installed without any user interaction", + QVariant(false))}; +} + +unsigned int InstallerQuick::priority() const +{ + return 50; +} + +bool InstallerQuick::isManualInstaller() const +{ + return false; +} + +bool InstallerQuick::isDataTextArchiveTopLayer(std::shared_ptr tree, + QString const& dataFolderName, + ModDataChecker*) const +{ + // A "DataText" archive is defined as having exactly one folder named like + // `dataFolderName` and one or more "useless" files (text files, pdf, or images). + static const std::set txtExtensions{ + "txt", "pdf", "md", "jpg", "jpeg", "png", "bmp"}; + bool dataFound = false; + bool txtFound = false; + for (auto entry : *tree) { + if (entry->isDir()) { + // If data was already found, or this is a directory not named "data", fail: + if (dataFound || entry->compare(dataFolderName) != 0) { + return false; + } + dataFound = true; + } else { + if (txtExtensions.count(entry->suffix()) == 0) { + return false; + } + txtFound = true; + } + } + return dataFound && txtFound; +} + +std::shared_ptr +InstallerQuick::getSimpleArchiveBase(std::shared_ptr dataTree, + QString const& dataFolderName, + ModDataChecker* checker) const +{ + if (!checker) { + return nullptr; + } + while (true) { + if (checker->dataLooksValid(dataTree) == ModDataChecker::CheckReturn::VALID || + isDataTextArchiveTopLayer(dataTree, dataFolderName, checker)) { + return dataTree; + } else if (dataTree->size() == 1 && dataTree->at(0)->isDir()) { + dataTree = dataTree->at(0)->astree(); + } else { + log::debug("Archive is not a simple archive."); + return nullptr; + } + } +} + +bool InstallerQuick::isArchiveSupported(std::shared_ptr tree) const +{ + auto checker = m_MOInfo->gameFeatures()->gameFeature(); + if (!checker) { + return false; + } + if (getSimpleArchiveBase(tree, m_MOInfo->managedGame()->dataDirectory().dirName(), + checker.get()) != nullptr) { + return true; + } + return checker->dataLooksValid(tree) == ModDataChecker::CheckReturn::FIXABLE; +} + +IPluginInstaller::EInstallResult +InstallerQuick::install(GuessedValue& modName, + std::shared_ptr& tree, QString&, int&) +{ + const QString dataFolderName = m_MOInfo->managedGame()->dataDirectory().dirName(); + auto checker = m_MOInfo->gameFeatures()->gameFeature(); + + auto base = std::const_pointer_cast( + getSimpleArchiveBase(tree, dataFolderName, checker.get())); + if (base == nullptr && + checker->dataLooksValid(tree) == ModDataChecker::CheckReturn::FIXABLE) { + tree = checker->fix(tree); + } else { + tree = base; + } + if (tree != nullptr) { + SimpleInstallDialog dialog(modName, parentWidget()); + if (m_MOInfo->pluginSetting(name(), "silent").toBool() || + dialog.exec() == QDialog::Accepted) { + modName.update(dialog.getName(), GUESS_USER); + + // If we have a data+txt archive, we move files to the data folder and + // switch to the data folder. We need to check that we actually have a + // checker here, otherwise it is anyway impossible that + // isDataTextArchiveTopLayer() returned true. + if (checker && isDataTextArchiveTopLayer(tree, dataFolderName, checker.get())) { + auto dataTree = tree->findDirectory(dataFolderName); + dataTree->detach(); + dataTree->merge(tree); + tree = dataTree; + } + return RESULT_SUCCESS; + } else { + if (dialog.manualRequested()) { + modName.update(dialog.getName(), GUESS_USER); + return RESULT_MANUALREQUESTED; + } else { + return RESULT_CANCELED; + } + } + } else { + // install shouldn't even have even have been called + qCritical("unsupported archive for quick installer"); + return RESULT_FAILED; + } +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(installerQuick, InstallerQuick) +#endif diff --git a/libs/installer_quick/src/installerquick.h b/libs/installer_quick/src/installerquick.h new file mode 100644 index 0000000..5d9c910 --- /dev/null +++ b/libs/installer_quick/src/installerquick.h @@ -0,0 +1,77 @@ +#ifndef INSTALLERQUICK_H +#define INSTALLERQUICK_H + +#include +#include + +class InstallerQuick : public MOBase::IPluginInstallerSimple +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.InstallerQuick") +#endif + +public: + InstallerQuick(); + + // Plugin functions: + virtual bool init(MOBase::IOrganizer* moInfo) override; + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + + // Installer functions: + virtual unsigned int priority() const override; + virtual bool isManualInstaller() const override; + virtual bool + isArchiveSupported(std::shared_ptr tree) const override; + + // Simple installer functions: + virtual EInstallResult install(MOBase::GuessedValue& modName, + std::shared_ptr& tree, + QString& version, int& modID) override; + +private: + /** + * @brief Check if the archive is a "DataText" archive. + * + * A "DataText" archive is defined as having exactly one folder named like the data + * folder of the game (`dataFolderName`) and one or more text or PDF files (standard + * package from french modding site). + * + * @param tree The tree to check. + * @param dataFolderName Name of the data folder (e.g., "data" for gamebryo games). + * @param checker The mod data checker, or a null pointer if none is available. + * + * @return true if the tree represents a "DataText" archive, false otherwise. + */ + bool isDataTextArchiveTopLayer(std::shared_ptr tree, + QString const& dataFolderName, + MOBase::ModDataChecker* checker) const; + + /** + * @brief Get the base of the archive. + * + * The base of the archive is either a "DataText" folder (i.e., a folder containing + * TXT or PDF files and a valid data folder), or an actual data folder. + * + * @param tree The tree to check. + * @param dataFolderName Name of the data folder (e.g., "data" for gamebryo games). + * @param checker The mod data checker, or a null pointer if none is available. + * + * @return the "base" of the archive, or a null pointer if none was found. + */ + std::shared_ptr + getSimpleArchiveBase(std::shared_ptr dataTree, + QString const& dataFolderName, + MOBase::ModDataChecker* checker) const; + +private: + const MOBase::IOrganizer* m_MOInfo; +}; + +#endif // INSTALLERQUICK_H diff --git a/libs/installer_quick/src/simpleinstalldialog.cpp b/libs/installer_quick/src/simpleinstalldialog.cpp new file mode 100644 index 0000000..b36b46e --- /dev/null +++ b/libs/installer_quick/src/simpleinstalldialog.cpp @@ -0,0 +1,69 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ +#include "simpleinstalldialog.h" +#include "ui_simpleinstalldialog.h" + +#include + +#include +#include + +using namespace MOBase; + +SimpleInstallDialog::SimpleInstallDialog(const GuessedValue& preset, + QWidget* parent) + : QDialog(parent), ui(new Ui::SimpleInstallDialog), m_Manual(false) +{ + ui->setupUi(this); + + for (auto iter = preset.variants().begin(); iter != preset.variants().end(); ++iter) { + ui->nameCombo->addItem(*iter); + } + + ui->nameCombo->setCurrentIndex(ui->nameCombo->findText(preset)); + + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + ui->nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); +} + +SimpleInstallDialog::~SimpleInstallDialog() +{ + delete ui; +} + +QString SimpleInstallDialog::getName() const +{ + return ui->nameCombo->currentText(); +} + +void SimpleInstallDialog::on_okBtn_clicked() +{ + this->accept(); +} + +void SimpleInstallDialog::on_cancelBtn_clicked() +{ + this->reject(); +} + +void SimpleInstallDialog::on_manualBtn_clicked() +{ + m_Manual = true; + this->reject(); +} diff --git a/libs/installer_quick/src/simpleinstalldialog.h b/libs/installer_quick/src/simpleinstalldialog.h new file mode 100644 index 0000000..e491438 --- /dev/null +++ b/libs/installer_quick/src/simpleinstalldialog.h @@ -0,0 +1,72 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef SIMPLEINSTALLDIALOG_H +#define SIMPLEINSTALLDIALOG_H + +#include +#include + +namespace Ui +{ +class SimpleInstallDialog; +} + +/** + * @brief Dialog for the installation of a simple archive + * a simple archive is one that doesn't require any manual changes to work correctly + **/ +class SimpleInstallDialog : public QDialog +{ + Q_OBJECT + +public: + /** + * @brief constructor + * + * @param preset suggested name for the mod + * @param parent parent widget + **/ + explicit SimpleInstallDialog(const MOBase::GuessedValue& preset, + QWidget* parent = 0); + ~SimpleInstallDialog(); + + /** + * @return true if the user requested the manual installation dialog + **/ + bool manualRequested() const { return m_Manual; } + /** + * @return the (user-modified) mod name + **/ + QString getName() const; + +private slots: + + void on_okBtn_clicked(); + + void on_cancelBtn_clicked(); + + void on_manualBtn_clicked(); + +private: + Ui::SimpleInstallDialog* ui; + bool m_Manual; +}; + +#endif // SIMPLEINSTALLDIALOG_H diff --git a/libs/installer_quick/src/simpleinstalldialog.ui b/libs/installer_quick/src/simpleinstalldialog.ui new file mode 100644 index 0000000..38a31ee --- /dev/null +++ b/libs/installer_quick/src/simpleinstalldialog.ui @@ -0,0 +1,86 @@ + + + SimpleInstallDialog + + + + 0 + 0 + 400 + 83 + + + + Quick Install + + + + + + + + Name + + + + + + + true + + + + + + + + + + + Opens a Dialog that allows custom modifications. + + + Opens a Dialog that allows custom modifications. + + + Manual + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + OK + + + true + + + + + + + Cancel + + + + + + + + + + diff --git a/libs/installer_quick/vcpkg.json b/libs/installer_quick/vcpkg.json new file mode 100644 index 0000000..28051ad --- /dev/null +++ b/libs/installer_quick/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673" + } + } +} diff --git a/libs/installer_wizard/.gitattributes b/libs/installer_wizard/.gitattributes new file mode 100644 index 0000000..9790024 --- /dev/null +++ b/libs/installer_wizard/.gitattributes @@ -0,0 +1,6 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.py text eol=crlf diff --git a/libs/installer_wizard/.github/workflows/build.yml b/libs/installer_wizard/.github/workflows/build.yml new file mode 100644 index 0000000..c486f62 --- /dev/null +++ b/libs/installer_wizard/.github/workflows/build.yml @@ -0,0 +1,39 @@ +name: Build Installer Wizard Plugin + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + runs-on: windows-2022 + steps: + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Set environmental variables + shell: bash + run: | + echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> $GITHUB_ENV + + - uses: actions/checkout@v4 + + - name: Configure Installer Wizard Plugin build + shell: pwsh + run: | + cmake --preset vs2022-windows "-DCMAKE_INSTALL_PREFIX=install" "-DVCPKG_MANIFEST_FEATURES=standalone" + + - name: Build Installer Wizard Plugin + run: cmake --build vsbuild --config RelWithDebInfo + + - name: Install Installer Wizard Plugin + run: cmake --install vsbuild --config RelWithDebInfo + + - name: Upload Installer Wizard Plugin artifact + uses: actions/upload-artifact@master + with: + name: installer_wizard + path: ./install/bin/plugins diff --git a/libs/installer_wizard/.github/workflows/linters.yml b/libs/installer_wizard/.github/workflows/linters.yml new file mode 100644 index 0000000..95f45e6 --- /dev/null +++ b/libs/installer_wizard/.github/workflows/linters.yml @@ -0,0 +1,32 @@ +name: linters + +on: [push, pull_request] + +jobs: + checks: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: [3.12] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - uses: abatilo/actions-poetry@v2 + - name: Install package + run: poetry install + + # generate UI file for linting + - name: Generate UI files + shell: pwsh + run: | + Get-ChildItem -Recurse -File -Include "*.ui" | ForEach-Object { + poetry run pyuic6 $_ -o ([io.path]::ChangeExtension($_.FullName, "py")) + } + + - name: Lint + run: poetry run poe lint diff --git a/libs/installer_wizard/.gitignore b/libs/installer_wizard/.gitignore new file mode 100644 index 0000000..b3abdd9 --- /dev/null +++ b/libs/installer_wizard/.gitignore @@ -0,0 +1,20 @@ +# Generated UI files - The __init__.py is also exclude but it has +# been added so should be fine: +src/ui/*.py + +# IDE / Tools files: +**/__pycache__ +**/.mypy_cache +.tox +.vscode +venv + +# MO2 build files: +vsbuild +mob.log + +# Generated files: +lib +src/lib +installer_wizard +installer_wizard-*.zip diff --git a/libs/installer_wizard/.pre-commit-config.yaml b/libs/installer_wizard/.pre-commit-config.yaml new file mode 100644 index 0000000..047c78c --- /dev/null +++ b/libs/installer_wizard/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + - id: mixed-line-ending + args: [--fix=lf] + - id: check-case-conflict + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.7 # must match pyproject.toml + hooks: + - id: ruff + args: [--extend-select, I, --fix] + - id: ruff-format + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/installer_wizard/CMakeLists.txt b/libs/installer_wizard/CMakeLists.txt new file mode 100644 index 0000000..2fb2ad5 --- /dev/null +++ b/libs/installer_wizard/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.16) + +project(installer_wizard LANGUAGES NONE) + +find_package(mo2-cmake CONFIG REQUIRED) + +add_subdirectory(src) diff --git a/libs/installer_wizard/CMakePresets.json b/libs/installer_wizard/CMakePresets.json new file mode 100644 index 0000000..acc6d97 --- /dev/null +++ b/libs/installer_wizard/CMakePresets.json @@ -0,0 +1,17 @@ +{ + "configurePresets": [ + { + "binaryDir": "${sourceDir}/vsbuild", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "generator": "Visual Studio 17 2022", + "name": "vs2022-windows" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/installer_wizard/LICENSE b/libs/installer_wizard/LICENSE new file mode 100644 index 0000000..45ca39c --- /dev/null +++ b/libs/installer_wizard/LICENSE @@ -0,0 +1,7 @@ +Copyright 2020 Holt59 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/libs/installer_wizard/README.md b/libs/installer_wizard/README.md new file mode 100644 index 0000000..b8db085 --- /dev/null +++ b/libs/installer_wizard/README.md @@ -0,0 +1,83 @@ +# MO2 BAIN Wizard Installer Plugin + +This plugin can be used to install BAIN archives containing a wizard script (`wizard.txt`). + +## How to install? + +Go to the [releases page](https://github.com/Holt59/modorganizer-installer_wizard/releases) and download +the latest release for your MO2 version. + +### A few words on INI Tweaks + +Mod Organizer 2 does not currently manage INI Tweaks, so the Wizard installer is partially functional +regarding them. + +- The installer will create proper INI Tweaks when requested, but these will not be applied + to the game INI files automatically. If INI Tweaks are present, a pop-up should appear at + the end of the installation. +- INI Tweaks for OBSE script are directly applied to the OBSE scripts. + +## How to contribute? + +### Setting-up the environment + +Below are the steps to setup a development environment. + + +1. Clone this repository into the Mod Organizer 2 plugins folder. + +```bash +# (Optional) you can change the name of the folder: +git clone https://github.com/Holt59/modorganizer-installer_wizard installer_wizard +``` + +2. **Requirements:** You need a Python 3.8 installation. The list of requirements is in + [`requirements.txt`](requirements.txt): + +```bash +# Those are only the development requirements. +pip install -r requirements +``` + +3. "Build" the installer: + +```bash +# This will install the 3rd party libraries in src/lib (required for the installer) and convert the .ui files into .py files. +make.ps1 +``` + +4. Create a root `__init__.py` - MO2 will not find and load the plugin unless there is a + `__init__.py` file in the root of the folder, so you need to create one: + +```python +from .src import createPlugin +``` + +### Opening a Pull-Request + +Once you are satisfied with your changes, you can +[open a pull-request](https://github.com/Holt59/modorganizer-installer_wizard/pulls). +Before doing so, you should check that your code is properly +formatted and clean: + +```bash +# The -vv option is mandatory, otherwise tox will crash... +tox -vv -e py38-lint +``` + +### The interpreter + +The interpreter used by the installer is from the +[`bain-wizard-interpreter`](https://github.com/Holt59/bain-wizard-interpreter) package. +For issues related to interpreter (i.e. the script is wrongly parsed), open issues on the interpreter repository. + +## License + +MIT License + +Copyright (c) 2020 Mikaël Capelle + +See [LICENSE](LICENSE) for more information. + +**Note:** The release archives contains external libraries that are under their +own LICENSE. diff --git a/libs/installer_wizard/make-release.ps1 b/libs/installer_wizard/make-release.ps1 new file mode 100644 index 0000000..dab186f --- /dev/null +++ b/libs/installer_wizard/make-release.ps1 @@ -0,0 +1,29 @@ +# Build everything first: +.\make.ps1 + +# Package: +$target = ".\installer_wizard\" + +Remove-Item -Recurse -Force -ErrorAction Ignore $target +New-Item -Path $target -Type Directory | Out-Null +Copy-Item -Recurse -Path .\src\* -Exclude "*.ui" -Destination $target +Get-ChildItem -Recurse $target -Include "__pycache__" | Remove-Item -Recurse -Force +Copy-Item .\installer_wizard_en.ts, .\README.md, .\LICENSE $target + +# Find the version: +$ctx = Get-Content .\src\installer.py | Select-String -Pattern "def version\(self\):" -Context 0, 1 +$parts = $ctx.Context[0].PostContext.Split("(")[1].Trim(")").Split(",").Trim() +$version = Join-String -Separator "." -InputObject $parts[0..2] + +if ($parts[3] -match "ALPHA") { + $version += "a" +} +if ($parts[3] -match "BETA") { + $version += "b" +} + +# Create the zip: +$archive = "installer_wizard-$version.zip" +Remove-Item -Force -ErrorAction Ignore $archive +Compress-Archive -Path $target -DestinationPath $archive +Write-Output "Created archive $archive." diff --git a/libs/installer_wizard/make.ps1 b/libs/installer_wizard/make.ps1 new file mode 100644 index 0000000..b6bfdb9 --- /dev/null +++ b/libs/installer_wizard/make.ps1 @@ -0,0 +1,10 @@ +# Install the lib: +pip install --target=.\src\lib --upgrade -r .\plugin-requirements.txt | Out-Null + +# Convert ui files: +Get-ChildItem -Recurse -File -Include "*.ui" | ForEach-Object { + pyuic5 $_ -o ([io.path]::ChangeExtension($_.FullName, "py")) +} + +# Generate the .ts file: +pylupdate5 (Get-ChildItem src -Exclude lib | Get-ChildItem -Recurse -File -Include "*.py") -ts installer_wizard_en.ts diff --git a/libs/installer_wizard/plugin-requirements.txt b/libs/installer_wizard/plugin-requirements.txt new file mode 100644 index 0000000..5203390 --- /dev/null +++ b/libs/installer_wizard/plugin-requirements.txt @@ -0,0 +1 @@ +bain-wizard-interpreter==1.0.4 diff --git a/libs/installer_wizard/poetry.lock b/libs/installer_wizard/poetry.lock new file mode 100644 index 0000000..b5a1efb --- /dev/null +++ b/libs/installer_wizard/poetry.lock @@ -0,0 +1,294 @@ +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. + +[[package]] +name = "antlr4-python3-runtime" +version = "4.13.1" +description = "ANTLR 4.13.1 runtime for Python 3" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "antlr4-python3-runtime-4.13.1.tar.gz", hash = "sha256:3cd282f5ea7cfb841537fe01f143350fdb1c0b1ce7981443a2fa8513fddb6d1a"}, + {file = "antlr4_python3_runtime-4.13.1-py3-none-any.whl", hash = "sha256:78ec57aad12c97ac039ca27403ad61cb98aaec8a3f9bb8144f889aa0fa28b943"}, +] + +[[package]] +name = "bain-wizard-interpreter" +version = "1.0.4" +description = "BAIN Wizard Interpreter based on wizparse." +optional = false +python-versions = "<4.0,>=3.11.1" +groups = ["main"] +files = [ + {file = "bain_wizard_interpreter-1.0.4-py3-none-any.whl", hash = "sha256:c6a084349caa6fd80d8fbf222869fb63df8ede337edbb8774dff557ae2a981bc"}, + {file = "bain_wizard_interpreter-1.0.4.tar.gz", hash = "sha256:4ebd36112690775bab6c921c90422c9e5432901ca90860f36c5ba0153bb82079"}, +] + +[package.dependencies] +antlr4-python3-runtime = ">=4.13.1,<5.0.0" +chardet = ">=5.2.0,<6.0.0" + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "mobase-stubs" +version = "2.5.2" +description = "PEP561 stub files for the mobase Python API." +optional = false +python-versions = "<4.0,>=3.12" +groups = ["dev"] +files = [ + {file = "mobase_stubs-2.5.2-py3-none-any.whl", hash = "sha256:6a2c1c95494e7dc0d0f51d4f8f178423c0d9904f823f3c44845315bff304b948"}, + {file = "mobase_stubs-2.5.2.tar.gz", hash = "sha256:70c15579828df3bce01746e51fee07176390cdc386facea2b156ea04b3de055c"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.0-py2.py3-none-any.whl", hash = "sha256:508ecec98f9f3330b636d4448c0f1a56fc68017c68f1e7857ebc52acf0eb879a"}, + {file = "nodeenv-1.9.0.tar.gz", hash = "sha256:07f144e90dae547bf0d4ee8da0ee42664a42a04e02ed68e06324348dafe4bdb1"}, +] + +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] + +[[package]] +name = "poethepoet" +version = "0.34.0" +description = "A task runner that works well with poetry." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "poethepoet-0.34.0-py3-none-any.whl", hash = "sha256:c472d6f0fdb341b48d346f4ccd49779840c15b30dfd6bc6347a80d6274b5e34e"}, + {file = "poethepoet-0.34.0.tar.gz", hash = "sha256:86203acce555bbfe45cb6ccac61ba8b16a5784264484195874da457ddabf5850"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +pyyaml = ">=6.0.2,<7.0" + +[package.extras] +poetry-plugin = ["poetry (>=1.2.0,<3.0.0) ; python_version < \"4.0\""] + +[[package]] +name = "pyqt6" +version = "6.7.1" +description = "Python bindings for the Qt cross platform application toolkit" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "PyQt6-6.7.1-1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7f397f4b38b23b5588eb2c0933510deb953d96b1f0323a916c4839c2a66ccccc"}, + {file = "PyQt6-6.7.1-1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2f202b7941aa74e5c7e1463a6f27d9131dbc1e6cabe85571d7364f5b3de7397"}, + {file = "PyQt6-6.7.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:f053378e3aef6248fa612c8afddda17f942fb63f9fe8a9aeb2a6b6b4cbb0eba9"}, + {file = "PyQt6-6.7.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0adb7914c732ad1dee46d9cec838a98cb2b11bc38cc3b7b36fbd8701ae64bf47"}, + {file = "PyQt6-6.7.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2d771fa0981514cb1ee937633dfa64f14caa902707d9afffab66677f3a73e3da"}, + {file = "PyQt6-6.7.1-cp38-abi3-win_amd64.whl", hash = "sha256:fa3954698233fe286a8afc477b84d8517f0788eb46b74da69d3ccc0170d3714c"}, + {file = "PyQt6-6.7.1.tar.gz", hash = "sha256:3672a82ccd3a62e99ab200a13903421e2928e399fda25ced98d140313ad59cb9"}, +] + +[package.dependencies] +PyQt6-Qt6 = ">=6.7.0,<6.8.0" +PyQt6-sip = ">=13.8,<14" + +[[package]] +name = "pyqt6-qt6" +version = "6.7.1" +description = "The subset of a Qt installation needed by PyQt6." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "PyQt6_Qt6-6.7.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:29622b5dd38740b4b6962e0c88d082d08fa10b64542ef5d911b04214aad70150"}, + {file = "PyQt6_Qt6-6.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbcfe66a57199c6a26542b8b9b2f2ee59d974db36293de335a1251f24c4fe917"}, + {file = "PyQt6_Qt6-6.7.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:9fbab2a96d72d77d16021e259ef86a1a3c87adb0e7eebcc92df0d39f3fdf7e27"}, + {file = "PyQt6_Qt6-6.7.1-py3-none-win_amd64.whl", hash = "sha256:590a2f30d15892b5259e6a17ecc0a755675b1b49f553d964e195e90094b44120"}, +] + +[[package]] +name = "pyqt6-sip" +version = "13.10.2" +description = "The sip module support for PyQt6" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyqt6_sip-13.10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8132ec1cbbecc69d23dcff23916ec07218f1a9bbbc243bf6f1df967117ce303e"}, + {file = "pyqt6_sip-13.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07f77e89d93747dda71b60c3490b00d754451729fbcbcec840e42084bf061655"}, + {file = "pyqt6_sip-13.10.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4ffa71ddff6ef031d52cd4f88b8bba08b3516313c023c7e5825cf4a0ba598712"}, + {file = "pyqt6_sip-13.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:e907394795e61f1174134465c889177f584336a98d7a10beade2437bf5942244"}, + {file = "pyqt6_sip-13.10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a6c2f168773af9e6c7ef5e52907f16297d4efd346e4c958eda54ea9135be18e"}, + {file = "pyqt6_sip-13.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1d3cc9015a1bd8c8d3e86a009591e897d4d46b0c514aede7d2970a2208749cd"}, + {file = "pyqt6_sip-13.10.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ddd578a8d975bfb5fef83751829bf09a97a1355fa1de098e4fb4d1b74ee872fc"}, + {file = "pyqt6_sip-13.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:061d4a2eb60a603d8be7db6c7f27eb29d9cea97a09aa4533edc1662091ce4f03"}, + {file = "pyqt6_sip-13.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:45ac06f0380b7aa4fcffd89f9e8c00d1b575dc700c603446a9774fda2dcfc0de"}, + {file = "pyqt6_sip-13.10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:83e6a56d3e715f748557460600ec342cbd77af89ec89c4f2a68b185fa14ea46c"}, + {file = "pyqt6_sip-13.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ccf197f8fa410e076936bee28ad9abadb450931d5be5625446fd20e0d8b27a6"}, + {file = "pyqt6_sip-13.10.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:37af463dcce39285e686d49523d376994d8a2508b9acccb7616c4b117c9c4ed7"}, + {file = "pyqt6_sip-13.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:c7b34a495b92790c70eae690d9e816b53d3b625b45eeed6ae2c0fe24075a237e"}, + {file = "pyqt6_sip-13.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:c80cc059d772c632f5319632f183e7578cd0976b9498682833035b18a3483e92"}, + {file = "pyqt6_sip-13.10.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8b5d06a0eac36038fa8734657d99b5fe92263ae7a0cd0a67be6acfe220a063e1"}, + {file = "pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad376a6078da37b049fdf9d6637d71b52727e65c4496a80b753ddc8d27526aca"}, + {file = "pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b"}, + {file = "pyqt6_sip-13.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277"}, + {file = "pyqt6_sip-13.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244"}, + {file = "pyqt6_sip-13.10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38b5823dca93377f8a4efac3cbfaa1d20229aa5b640c31cf6ebbe5c586333808"}, + {file = "pyqt6_sip-13.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5506b9a795098df3b023cc7d0a37f93d3224a9c040c43804d4bc06e0b2b742b0"}, + {file = "pyqt6_sip-13.10.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e455a181d45a28ee8d18d42243d4f470d269e6ccdee60f2546e6e71218e05bb4"}, + {file = "pyqt6_sip-13.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:9c67ed66e21b11e04ffabe0d93bc21df22e0a5d7e2e10ebc8c1d77d2f5042991"}, + {file = "pyqt6_sip-13.10.2.tar.gz", hash = "sha256:464ad156bf526500ce6bd05cac7a82280af6309974d816739b4a9a627156fafe"}, +] + +[[package]] +name = "pyright" +version = "1.1.401" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pyright-1.1.401-py3-none-any.whl", hash = "sha256:6fde30492ba5b0d7667c16ecaf6c699fab8d7a1263f6a18549e0b00bf7724c06"}, + {file = "pyright-1.1.401.tar.gz", hash = "sha256:788a82b6611fa5e34a326a921d86d898768cddf59edde8e93e56087d277cc6f1"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" +typing-extensions = ">=4.1" + +[package.extras] +all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] +nodejs = ["nodejs-wheel-binaries"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "ruff" +version = "0.11.11" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:9924e5ae54125ed8958a4f7de320dab7380f6e9fa3195e3dc3b137c6842a0092"}, + {file = "ruff-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8a93276393d91e952f790148eb226658dd275cddfde96c6ca304873f11d2ae4"}, + {file = "ruff-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6e333dbe2e6ae84cdedefa943dfd6434753ad321764fd937eef9d6b62022bcd"}, + {file = "ruff-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7885d9a5e4c77b24e8c88aba8c80be9255fa22ab326019dac2356cff42089fc6"}, + {file = "ruff-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b5ab797fcc09121ed82e9b12b6f27e34859e4227080a42d090881be888755d4"}, + {file = "ruff-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e231ff3132c1119ece836487a02785f099a43992b95c2f62847d29bace3c75ac"}, + {file = "ruff-0.11.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a97c9babe1d4081037a90289986925726b802d180cca784ac8da2bbbc335f709"}, + {file = "ruff-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8c4ddcbe8a19f59f57fd814b8b117d4fcea9bee7c0492e6cf5fdc22cfa563c8"}, + {file = "ruff-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6224076c344a7694c6fbbb70d4f2a7b730f6d47d2a9dc1e7f9d9bb583faf390b"}, + {file = "ruff-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:882821fcdf7ae8db7a951df1903d9cb032bbe838852e5fc3c2b6c3ab54e39875"}, + {file = "ruff-0.11.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dcec2d50756463d9df075a26a85a6affbc1b0148873da3997286caf1ce03cae1"}, + {file = "ruff-0.11.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99c28505ecbaeb6594701a74e395b187ee083ee26478c1a795d35084d53ebd81"}, + {file = "ruff-0.11.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9263f9e5aa4ff1dec765e99810f1cc53f0c868c5329b69f13845f699fe74f639"}, + {file = "ruff-0.11.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:64ac6f885e3ecb2fdbb71de2701d4e34526651f1e8503af8fb30d4915a3fe345"}, + {file = "ruff-0.11.11-py3-none-win32.whl", hash = "sha256:1adcb9a18802268aaa891ffb67b1c94cd70578f126637118e8099b8e4adcf112"}, + {file = "ruff-0.11.11-py3-none-win_amd64.whl", hash = "sha256:748b4bb245f11e91a04a4ff0f96e386711df0a30412b9fe0c74d5bdc0e4a531f"}, + {file = "ruff-0.11.11-py3-none-win_arm64.whl", hash = "sha256:6c51f136c0364ab1b774767aa8b86331bd8e9d414e2d107db7a2189f35ea1f7b"}, + {file = "ruff-0.11.11.tar.gz", hash = "sha256:7774173cc7c1980e6bf67569ebb7085989a78a103922fb83ef3dfe230cd0687d"}, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] + +[metadata] +lock-version = "2.1" +python-versions = "^3.12" +content-hash = "32e576ced1cd9fd1078bf3656ed1726be6e76a63e31498a891a463e5c2078e60" diff --git a/libs/installer_wizard/pyproject.toml b/libs/installer_wizard/pyproject.toml new file mode 100644 index 0000000..b4fadb6 --- /dev/null +++ b/libs/installer_wizard/pyproject.toml @@ -0,0 +1,65 @@ +[project] +name = "installer-wizard" +version = "1.0.3" +description = "" +authors = [{ name = "Mikaël Capelle", email = "capelle.mikael@gmail.com" }] +license = "MIT" +readme = "README.md" +dynamic = ["dependencies"] + +[tool.poetry] +package-mode = false + +[tool.poetry.dependencies] +python = "^3.12" +pyqt6 = "6.7.1" +bain-wizard-interpreter = "1.0.4" + +[tool.poetry.group.dev.dependencies] +pyright = "^1.1.401" +ruff = "^0.11.11" +poethepoet = "^0.34.0" +mobase-stubs = {version = "2.5.2", allow-prereleases = true} + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poe.tasks] +format-imports = "ruff check --select I . --fix" +format-ruff = "ruff format ." +format.sequence = ["format-imports", "format-ruff"] +lint-ruff = "ruff check ." +lint-ruff-format = "ruff format --check ." +lint-pyright = "pyright ." +lint.sequence = ["lint-ruff", "lint-ruff-format", "lint-pyright"] +lint.ignore_fail = "return_non_zero" + +[tool.ruff] +target-version = "py312" + +[tool.ruff.lint] +extend-select = ["B", "Q", "I"] + +[tool.ruff.lint.isort.sections] +mobase = ["mobase"] +wizard = ["wizard"] + +[tool.ruff.lint.isort] +required-imports = ["from __future__ import annotations"] +section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "wizard", + "mobase", + "local-folder", +] + +[tool.pyright] +exclude = ["lib", "venv", "src/ui"] +typeCheckingMode = "strict" +reportMissingTypeStubs = true +reportMissingModuleSource = false +pythonPlatform = "Windows" diff --git a/libs/installer_wizard/src/CMakeLists.txt b/libs/installer_wizard/src/CMakeLists.txt new file mode 100644 index 0000000..b772dea --- /dev/null +++ b/libs/installer_wizard/src/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.16) + +add_custom_target(installer_wizard ALL) +mo2_configure_python(installer_wizard MODULE) diff --git a/libs/installer_wizard/src/__init__.py b/libs/installer_wizard/src/__init__.py new file mode 100644 index 0000000..2b3b391 --- /dev/null +++ b/libs/installer_wizard/src/__init__.py @@ -0,0 +1,17 @@ +""" +This file is the entry point of the module and must contain a createPlugin() +or createPlugins() function. +""" + +from __future__ import annotations + +import os +import site + +site.addsitedir(os.path.join(os.path.dirname(__file__), "lib")) + +from .installer import WizardInstaller # noqa: E402 + + +def createPlugin() -> WizardInstaller: + return WizardInstaller() diff --git a/libs/installer_wizard/src/dialog.py b/libs/installer_wizard/src/dialog.py new file mode 100644 index 0000000..76d10bb --- /dev/null +++ b/libs/installer_wizard/src/dialog.py @@ -0,0 +1,752 @@ +from __future__ import annotations + +from collections.abc import Sequence +from pathlib import Path +from typing import Any, cast + +from antlr4 import ParserRuleContext +from PyQt6 import QtWidgets +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QFontDatabase, QKeySequence, QPixmap, QResizeEvent, QShortcut +from PyQt6.QtWidgets import QApplication + +from wizard.contexts import ( + WizardInterpreterContext, + WizardRequireVersionsContext, + WizardSelectContext, + WizardSelectManyContext, + WizardSelectOneContext, + WizardTerminationContext, + WizardTopLevelContext, +) +from wizard.errors import WizardError +from wizard.interpreter import WizardInterpreter +from wizard.manager import SelectOption +from wizard.runner import WizardRunnerKeywordVisitor, WizardRunnerState +from wizard.tweaks import WizardINISetting +from wizard.value import Plugin + +import mobase + +from .ui.wizardinstallercomplete import Ui_WizardInstallerComplete +from .ui.wizardinstallerdialog import Ui_WizardInstallerDialog +from .ui.wizardinstallererror import Ui_WizardInstallerError +from .ui.wizardinstallerpage import Ui_WizardInstallerPage +from .ui.wizardinstallerrequires import Ui_WizardInstallerRequires +from .utils import make_ini_tweaks + +WizardRunnerContext = WizardInterpreterContext[WizardRunnerState, Any] + + +def check_version( + context: WizardRequireVersionsContext[WizardRunnerState], + organizer: mobase.IOrganizer, +) -> tuple[bool, bool, bool, bool]: + """ + Check if the requirements are ok. + + Args: + context: The requires version context to check. + organizer: The organizer to fetch actual versions from. + + Returns: + A 4-tuple of boolean values, where each value is True if the installed + version is ok, False otherwise. In order, checks are game, script extender + graphics extender (True if there is no requirements, False otherwise since + we cannot check in MO2), and wrye bash (always True). + """ + game = organizer.managedGame() + + game_ok = True + if context.game_version: + game_ok = mobase.VersionInfo(context.game_version) <= game.version() + + # script extender + se_ok = True + if context.script_extender_version: + se = organizer.gameFeatures().gameFeature(mobase.ScriptExtender) + if not se or not se.isInstalled(): + se_ok = False + else: + if mobase.VersionInfo( + context.script_extender_version + ) <= mobase.VersionInfo(se.getExtenderVersion()): + se_ok = True + else: + se_ok = False + + # cannot check these so... + ge_ok = not context.graphics_extender_version + + return (game_ok, se_ok, ge_ok, True) + + +class WizardInstallerRequiresVersionPage(QtWidgets.QWidget): + context: WizardRequireVersionsContext[WizardRunnerState] + + def __init__( + self, + context: WizardRequireVersionsContext[WizardRunnerState], + organizer: mobase.IOrganizer, + parent: QtWidgets.QWidget, + ): + super().__init__(parent) + + self.context = context + + # set the ui file + self.ui = Ui_WizardInstallerRequires() + self.ui.setupUi(self) # pyright: ignore[reportUnknownMemberType] + + self.ui.groupBox.setStyleSheet( + 'QLabel[headercell="true"] { font-weight: bold; }' + ) + + game = organizer.managedGame() + + okIcon = QPixmap(":/MO/gui/checked-checkbox").scaled( + 16, + 16, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + noIcon = QPixmap(":/MO/gui/unchecked-checkbox").scaled( + 16, + 16, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + koIcon = QPixmap(":/MO/gui/indeterminate-checkbox").scaled( + 16, + 16, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + + self.ui.labelGame.setText(game.gameName()) + + # set the required version + self.ui.labelGameNeed.setText(context.game_version) + self.ui.labelScriptExtenderNeed.setText(context.script_extender_version) + self.ui.labelGraphicsExtenderNeed.setText(context.graphics_extender_version) + self.ui.labelWryeBashNeed.setText(context.wrye_bash_version) + + # set the current version + self.ui.labelGameHave.setText(game.version().canonicalString()) + se = organizer.gameFeatures().gameFeature(mobase.ScriptExtender) + if se and se.isInstalled(): + self.ui.labelScriptExtenderHave.setText(se.getExtenderVersion()) + + # cannot check these so... + game_ok, se_ok, _, _ = check_version(context, organizer) + self.ui.labelGameIcon.setPixmap(okIcon if game_ok else koIcon) + self.ui.labelScriptExtenderIcon.setPixmap(okIcon if se_ok else koIcon) + self.ui.labelGraphicsExtenderIcon.setPixmap(noIcon) + self.ui.labelWryeBashIcon.setPixmap(noIcon) + + +class WizardInstallerSelectPage(QtWidgets.QWidget): + # signal emitted when an item is double-clicked, only for SelectOne context + itemDoubleClicked = pyqtSignal() + + context: WizardSelectContext[WizardRunnerState, Any] + _images: dict[Path, Path] + _currentImage: QPixmap + + def __init__( + self, + context: WizardSelectContext[WizardRunnerState, Any], + images: dict[Path, Path], + options: Sequence[str] | None, + parent: QtWidgets.QWidget, + ): + """ + Args: + context: The context for this page. + images: A mapping from path (in the archive) to extracted path. + options: Potential list of options to select. Might not exactly match. + parent: The parent widget. + """ + super().__init__(parent) + + self._images = images + + # set the ui file + self.ui = Ui_WizardInstallerPage() + self.ui.setupUi(self) # pyright: ignore[reportUnknownMemberType] + + self.ui.optionList.currentItemChanged.connect( # pyright: ignore[reportUnknownMemberType] + self.onCurrentItemChanged + ) + + # create list item widgets + for _ in context.options: + item = QtWidgets.QListWidgetItem() + self.ui.optionList.addItem(item) + + self.update_context(context) + + # extract previous select options + previous_options = [] + if options: + previous_options = [ + option for option in context.options if option.name in options + ] + else: + if isinstance(context, WizardSelectManyContext): + previous_options = context.defaults + elif isinstance(context, WizardSelectOneContext): + previous_options = [context.default] + + # set default values + for i, option in enumerate(context.options): + item = self.ui.optionList.item(i) + assert item is not None + if isinstance(context, WizardSelectManyContext): + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) + if option in previous_options: + item.setCheckState(Qt.CheckState.Checked) + else: + item.setCheckState(Qt.CheckState.Unchecked) + elif ( + isinstance(context, WizardSelectOneContext) + and option in previous_options + ): + item.setSelected(True) + self.ui.optionList.setCurrentItem(item) + + if isinstance(context, WizardSelectOneContext): + self.ui.optionList.doubleClicked.connect( # pyright: ignore[reportUnknownMemberType] + self.itemDoubleClicked.emit + ) + + def update_context(self, context: WizardSelectContext[WizardRunnerState, Any]): + self.context = context + + options = self.context.options + assert len(options) == self.ui.optionList.count() + + self.ui.selectDescriptionLabel.setText(context.description) + self.ui.selectDescriptionLabel.setMargin(4) + + # update the content of the items + for i, option in enumerate(options): + item = self.ui.optionList.item(i) + assert item is not None + item.setText(option.name) + item.setData(Qt.ItemDataRole.UserRole, option) + + # no item selected, select the first one + if not self.ui.optionList.currentItem(): + self.ui.optionList.setCurrentRow(0) + + def onCurrentItemChanged( + self, current: QtWidgets.QListWidgetItem, previous: QtWidgets.QListWidgetItem + ): + option: SelectOption = current.data(Qt.ItemDataRole.UserRole) + self.ui.descriptionTextEdit.setText(option.description) + image = option.image + if image and Path(image) in self._images: + target = self._images[Path(image)] + self._currentImage = QPixmap(target.as_posix()) + else: + self._currentImage = QPixmap() + + self.ui.imageLabel.setPixmap(self.getResizedImage()) + + def getResizedImage(self) -> QPixmap: + if self._currentImage.isNull(): + return self._currentImage + return self._currentImage.scaled( + self.ui.imageLabel.size(), + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + + def resizeEvent(self, a0: QResizeEvent | None) -> None: + super().resizeEvent(a0) + self.ui.imageLabel.setPixmap(self.getResizedImage()) + + def selectedOptions(self) -> list[SelectOption]: + options: list[SelectOption] = [] + if isinstance(self.context, WizardSelectOneContext): + item = self.ui.optionList.currentItem() + assert item is not None + options.append(item.data(Qt.ItemDataRole.UserRole)) + else: + for i in range(self.ui.optionList.count()): + item = self.ui.optionList.item(i) + assert item is not None + if item.checkState() == Qt.CheckState.Checked: + options.append(item.data(Qt.ItemDataRole.UserRole)) + return options + + def selected(self) -> WizardSelectContext[WizardRunnerState, Any]: + if isinstance(self.context, WizardSelectOneContext): + return self.context.select(self.selectedOptions()[0]) + elif isinstance(self.context, WizardSelectManyContext): + return self.context.select(self.selectedOptions()) + else: + return self.context + + +class WizardInstallerCompletePage(QtWidgets.QWidget): + def __init__( + self, + context: WizardTerminationContext[WizardRunnerState], + parent: QtWidgets.QWidget, + ): + super().__init__(parent) + + # set the ui file + self.ui = Ui_WizardInstallerComplete() + self.ui.setupUi(self) # pyright: ignore[reportUnknownMemberType] + + self.setStyleSheet('QLabel[heading="true"] { font-weight: bold; }') + + self.context = context + self.state = context.state + + # retrieve the keyword visitor + kvisitor = cast(WizardRunnerKeywordVisitor, context.factory.kvisitor) + + # the list of plugins in selected sub-packages + plugins: set[Plugin] = set() + + # sub-packages + for sp in kvisitor.subpackages: + item = QtWidgets.QListWidgetItem() + item.setText(sp.name) + if sp.name in self.state.subpackages: + item.setCheckState(Qt.CheckState.Checked) + else: + item.setCheckState(Qt.CheckState.Unchecked) + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) + plugins.update(kvisitor.plugins_for(sp)) + self.ui.subpackagesList.addItem(item) + + # switch the renamed plugins + for plugin in list(plugins): + if plugin in self.state.renames: + plugins.remove(plugin) + plugins.add(Plugin(self.state.renames[plugin])) + + # lugins + for plugin in sorted(plugins): + item = QtWidgets.QListWidgetItem() + item.setText(plugin.name) + if plugin in self.state.plugins: + item.setCheckState(Qt.CheckState.Checked) + else: + item.setCheckState(Qt.CheckState.Unchecked) + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) + self.ui.pluginsList.addItem(item) + + # INI Tweaks + self.ui.tweaksWidget.setVisible(bool(self.state.tweaks)) + self.ui.tweaksList.currentItemChanged.connect( # pyright: ignore[reportUnknownMemberType] + self.onCurrentTweakItemChanged + ) + if self.state.tweaks: + # group the tweaks per file + tweaks = { + file: self.state.tweaks.tweaks(file) + for file in self.state.tweaks.files() + } + + for file, ftweaks in tweaks.items(): + item = QtWidgets.QListWidgetItem() + item.setText(file.replace("\\", "/")) + item.setData(Qt.ItemDataRole.UserRole, ftweaks) + self.ui.tweaksList.addItem(item) + + self.ui.tweaksTextEdit.setFont( + QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) + ) + self.ui.tweaksList.setCurrentRow(0) + + # notes + md = "" + for note in self.state.notes: + md += f"- {note}\n" + document = self.ui.notesTextEdit.document() + assert document is not None + document.setIndentWidth(10) + self.ui.notesTextEdit.setMarkdown(md) + + def onCurrentTweakItemChanged( + self, current: QtWidgets.QListWidgetItem, previous: QtWidgets.QListWidgetItem + ): + # clear text area and create the tweaks + self.ui.tweaksTextEdit.clear() + self.ui.tweaksTextEdit.appendPlainText( + make_ini_tweaks(current.data(Qt.ItemDataRole.UserRole)) + ) + + def subpackages(self) -> list[str]: + """ + Returns: + The list of subpackages selected in the UI (either automatically by the + interpreter or by the user). + """ + sp: list[str] = [] + for i in range(self.ui.subpackagesList.count()): + item = self.ui.subpackagesList.item(i) + assert item is not None + if item.checkState() == Qt.CheckState.Checked: + sp.append(item.text()) + return sp + + def plugins(self) -> dict[str, bool]: + """ + Returns: + The list of plugins selected in the UI (either automatically by the + interpreter or by the user). + """ + sp: dict[str, bool] = {} + for i in range(self.ui.pluginsList.count()): + item = self.ui.pluginsList.item(i) + assert item is not None + sp[item.text()] = item.checkState() == Qt.CheckState.Checked + return sp + + def tweaks(self) -> dict[str, list[WizardINISetting]]: + """ + Returns: + The list of tweaks created by the wizard. The returned value maps filenames + to INI tweaks. + """ + rets: dict[str, list[WizardINISetting]] = {} + for i in range(self.ui.tweaksList.count()): + item = self.ui.tweaksList.item(i) + assert item is not None + rets[item.text()] = item.data(Qt.ItemDataRole.UserRole) + return rets + + +class WizardInstallerCancelPage(QtWidgets.QWidget): + def __init__( + self, + context: WizardTerminationContext[WizardRunnerState], + parent: QtWidgets.QWidget, + ): + super().__init__(parent) + + # set the ui file (same UI file for both cancel and error) + self.ui = Ui_WizardInstallerError() + self.ui.setupUi(self) # pyright: ignore[reportUnknownMemberType] + + self.ui.titleLabel.setText( + "The installation was cancelled by the installer with the following reason." + ) + style = self.style() + assert style is not None + self.ui.iconLabel.setPixmap( + style.standardIcon( + QtWidgets.QStyle.StandardPixmap.SP_MessageBoxWarning + ).pixmap(24, 24) + ) + self.ui.messageEdit.setText(context.message()) + + +class WizardInstallerErrorPage(QtWidgets.QWidget): + def __init__( + self, + error: WizardError, + parent: QtWidgets.QWidget, + ): + super().__init__(parent) + + # set the ui file (same UI file for both cancel and error) + self.ui = Ui_WizardInstallerError() + self.ui.setupUi(self) # pyright: ignore[reportUnknownMemberType] + + self.ui.titleLabel.setText( + "An error occurred during the installation of the script, " + "this is probably due to an incorrect script file (wizard.txt) in the " + "archive." + ) + style = self.style() + assert style is not None + self.ui.iconLabel.setPixmap( + style.standardIcon( + QtWidgets.QStyle.StandardPixmap.SP_MessageBoxCritical + ).pixmap(24, 24) + ) + self.ui.messageEdit.setText(str(error)) + + +class WizardInstallerDialog(QtWidgets.QDialog): + # flag to indicate if the user chose to do a manual installation + _manual: bool = False + + # organizer + _organizer: mobase.IOrganizer + + # interpreter + _interpreter: WizardInterpreter[WizardRunnerState] + _images: dict[Path, Path] + _options: dict[str, list[str]] + + # the Wizard MO2 interface + _start_context: WizardTopLevelContext[WizardRunnerState] + + # dict from context to selected options + _pages: dict[ParserRuleContext, WizardInstallerSelectPage] + + def __init__( + self, + organizer: mobase.IOrganizer, + interpreter: WizardInterpreter[WizardRunnerState], + context: WizardTopLevelContext[WizardRunnerState], + name: mobase.GuessedString, + images: dict[Path, Path], + options: dict[str, list[str]], + parent: QtWidgets.QWidget, + ): + """ + Args: + interpreter: The interpreter to use. + context: The initial context of the script. + name: The name of the mod. + images: A mapping from path (in the archive) to extracted path. + options: The previously selected options. + parent: The parent widget. + """ + super().__init__(parent) + + self._organizer = organizer + self._interpreter = interpreter + self._images = images + self._options = options + self._start_context = context + self._pages = {} + + # set the ui file + self.ui = Ui_WizardInstallerDialog() + self.ui.setupUi(self) # pyright: ignore[reportUnknownMemberType] + + self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False) + self.setWindowFlag(Qt.WindowType.WindowMaximizeButtonHint, True) + + # mobase.GuessedString contains multiple names with various level of + # "guess", using.variants() returns the list of names, and doing str(name) + # will return the most-likely value + for value in name.variants(): + self.ui.nameCombo.addItem(value) + completer = self.ui.nameCombo.completer() + assert completer is not None + completer.setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive) + self.ui.nameCombo.setCurrentIndex(self.ui.nameCombo.findText(str(name))) + + # we need to connect the Cancel / Manual buttons. We can of course use + # PyQt6 signal/slot syntax + self.ui.cancelBtn.clicked.connect( # pyright: ignore[reportUnknownMemberType] + self.reject + ) + + def manualClicked(): + self._manual = True + self.reject() + + self.ui.manualBtn.clicked.connect( # pyright: ignore[reportUnknownMemberType] + manualClicked + ) + + self.ui.prevBtn.clicked.connect( # pyright: ignore[reportUnknownMemberType] + self.previousClicked + ) + self.ui.nextBtn.clicked.connect( # pyright: ignore[reportUnknownMemberType] + self.nextClicked + ) + + backShortcut = QShortcut(QKeySequence(Qt.Key.Key_Backspace), self) + backShortcut.activated.connect( # pyright: ignore[reportUnknownMemberType] + self.previousClicked + ) + + @property + def scriptButtonClicked(self) -> pyqtSignal: + return self.ui.scriptBtn.clicked # pyright: ignore[reportReturnType] + + def name(self): + return self.ui.nameCombo.currentText() + + def subpackages(self): + """ + Returns: + The list of subpackages to install. Only valid if exec() returned + Accepted. + """ + # we cannot fetch it from the state since the user can modify it in the UI + widget = self.ui.stackedWidget.currentWidget() + assert isinstance(widget, WizardInstallerCompletePage) + return widget.subpackages() + + def plugins(self) -> dict[str, bool]: + """ + Returns: + The list of plugins to install and enable. Only valid if exec() returned + Accepted. + """ + # we cannot fetch it from the state since the user can modify it in the UI + widget = self.ui.stackedWidget.currentWidget() + assert isinstance(widget, WizardInstallerCompletePage) + return widget.plugins() + + def renames(self) -> dict[str, str]: + """ + Returns: + The mapping of renames for plugins. Only valid if exec() returned Accepted. + """ + widget = self.ui.stackedWidget.currentWidget() + assert isinstance(widget, WizardInstallerCompletePage) + return { + plugin.name: new_name for plugin, new_name in widget.state.renames.items() + } + + def tweaks(self) -> dict[str, list[WizardINISetting]]: + """ + Returns: + The list of tweaks per file. Only valid if exec() returned Accepted. + """ + widget = self.ui.stackedWidget.currentWidget() + assert isinstance(widget, WizardInstallerCompletePage) + return widget.tweaks() + + def selectedOptions(self) -> dict[str, list[str]]: + """ + Returns: + The list of all currently selected options. + """ + result: dict[str, list[str]] = {} + for i in range(self.ui.stackedWidget.count()): + page = self.ui.stackedWidget.widget(i) + if isinstance(page, WizardInstallerSelectPage): + result[page.context.description] = [ + option.name for option in page.selectedOptions() + ] + return result + + def isManualRequested(self): + return self._manual + + def previousClicked(self): + index = self.ui.stackedWidget.currentIndex() + if index > 0: + self.ui.stackedWidget.removeWidget(self.ui.stackedWidget.widget(index)) + + self._update_prev_button() + self._update_next_button() + self._update_focus() + + def nextClicked(self): + widget = self.ui.stackedWidget.currentWidget() + + try: + if isinstance(widget, WizardInstallerSelectPage): + context = widget.selected().exec() + elif isinstance(widget, WizardInstallerRequiresVersionPage): + context = widget.context.exec() + else: + self.accept() + return + + context = self._exec_until(context) + + if context.context in self._pages: + page = self._pages[context.context] + assert isinstance(context, WizardSelectContext) + page.update_context(context) + else: + page = self._make_page(context) + + except WizardError as ex: + page = WizardInstallerErrorPage(ex, self) + + index = self.ui.stackedWidget.addWidget(page) + self.ui.stackedWidget.setCurrentIndex(index) + self._update_prev_button() + self._update_next_button() + self._update_focus() + + def _update_focus(self): + widget = self.ui.stackedWidget.currentWidget() + if isinstance(widget, WizardInstallerSelectPage): + widget.ui.optionList.setFocus() + + def _update_prev_button(self): + self.ui.prevBtn.setDisabled(self.ui.stackedWidget.currentIndex() <= 0) + + def _update_next_button(self): + widget = self.ui.stackedWidget.currentWidget() + + self.ui.nextBtn.setDisabled(False) + + name: str = self.ui.nextBtn.text() + if isinstance(widget, WizardInstallerSelectPage): + name = self.tr("Next") + elif isinstance(widget, WizardInstallerRequiresVersionPage): + name = self.tr("Install anyway") + elif isinstance(widget, (WizardInstallerCancelPage, WizardInstallerErrorPage)): + self.ui.nextBtn.setDisabled(True) + else: + name = self.tr("Install") + + self.ui.nextBtn.setText(name) + + def _exec_until(self, context: WizardRunnerContext) -> WizardRunnerContext: + context = self._interpreter.exec_until( + context, + ( + WizardSelectContext, + WizardRequireVersionsContext, + ), + ) + + # if all requirements are ok, skip the context + if isinstance(context, WizardRequireVersionsContext): + if all(check_version(context, self._organizer)): + return self._exec_until(context.exec()) + + return context + + def _make_page(self, context: WizardRunnerContext) -> QtWidgets.QWidget: + page: QtWidgets.QWidget + if isinstance(context, WizardSelectContext): + page = WizardInstallerSelectPage( + context, + self._images, + self._options.get(context.description, None), + self, + ) + page.itemDoubleClicked.connect( # pyright: ignore[reportUnknownMemberType] + self.nextClicked + ) + self._pages[context.context] = page # type: ignore + elif isinstance(context, WizardRequireVersionsContext): + page = WizardInstallerRequiresVersionPage(context, self._organizer, self) + elif isinstance(context, WizardTerminationContext): + if context.is_cancel(): + page = WizardInstallerCancelPage(context, self) + else: + page = WizardInstallerCompletePage(context, self) + else: + raise NotImplementedError() # for typing purpose + + return page + + def exec(self): + try: + context = self._exec_until(self._start_context) + page = self._make_page(context) + except WizardError as ex: + page = WizardInstallerErrorPage(ex, self) + self.ui.stackedWidget.addWidget(page) + self._update_prev_button() + self._update_next_button() + self._update_focus() + return super().exec() + + def tr(self, value: str): # pyright: ignore[reportIncompatibleMethodOverride] + return QApplication.translate("WizardInstallerDialog", value) diff --git a/libs/installer_wizard/src/installer.py b/libs/installer_wizard/src/installer.py new file mode 100644 index 0000000..c32956c --- /dev/null +++ b/libs/installer_wizard/src/installer.py @@ -0,0 +1,397 @@ +from __future__ import annotations + +import os +import re +import sys +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Optional, Sequence, Union, cast + +from PyQt6 import QtWidgets +from PyQt6.QtWidgets import QApplication + +from wizard.runner import WizardRunnerState + +import mobase + +from .dialog import WizardInstallerDialog +from .runner import make_interpreter +from .utils import make_ini_tweaks, merge_ini_tweaks + + +class WizardInstaller(mobase.IPluginInstallerSimple): + """ + This is the actual plugin. MO2 has two types of installer plugin, this one is + "simple", i.e., it will work directly on the file-tree contained in the archive. + The purpose of the installer is to take the file-tree from the archive, check if + it is valid (for this installer) and then modify it if required before extraction. + """ + + # regex used to parse settings + RE_DESCRIPTION = re.compile(r"select([0-9]+)-description") + RE_OPTION = re.compile(r"select([0-9]+)-option([0-9]+)") + + _organizer: mobase.IOrganizer + + # list of selected options + _installerOptions: Dict[str, List[str]] + _installerUsed: bool + + def __init__(self): + super().__init__() + + def init(self, organizer: mobase.IOrganizer): + self._organizer = organizer + return True + + def name(self): + return "BAIN Wizard Installer" + + def localizedName(self) -> str: + return self.tr("BAIN Wizard Installer") + + def author(self): + return "Holt59" + + def description(self): + return self.tr("Installer for BAIN archive containing wizard scripts.") + + def version(self): + return mobase.VersionInfo(1, 0, 2) + + def isActive(self): + return self._organizer.pluginSetting(self.name(), "enabled") + + def settings(self): + return [ + mobase.PluginSetting("enabled", "check to enable this plugin", True), + mobase.PluginSetting( + "prefer_fomod", + "prefer FOMOD installer over this one when possible", + True, + ), + mobase.PluginSetting( + "prefer_omod", + "prefer OMOD installer over this one when possible", + False, + ), + # above FOMOD? + mobase.PluginSetting("priority", "priority of this installer", 120), + ] + + # method for IPluginInstallerSimple + + def priority(self) -> int: + return cast(int, self._organizer.pluginSetting(self.name(), "priority")) + + def isManualInstaller(self) -> bool: + return False + + def onInstallationStart( + self, + archive: str, + reinstallation: bool, + current_mod: Optional[mobase.IModInterface], + ): + self._installerUsed = False + self._installerOptions = {} + + if current_mod: + settings = current_mod.pluginSettings(self.name()) + + # first extract the description + descriptions: Dict[int, str] = {} + options: Dict[int, Dict[int, str]] = defaultdict(dict) + for setting, value in settings.items(): + mdesc = WizardInstaller.RE_DESCRIPTION.match(setting) + if mdesc: + select = int(mdesc.group(1)) + descriptions[select] = str(value) + + mopt = WizardInstaller.RE_OPTION.match(setting) + if mopt: + select = int(mopt.group(1)) + index = int(mopt.group(2)) + options[select][index] = str(value) + + for kdesc, desc in descriptions.items(): + self._installerOptions[desc] = [] + if kdesc in options: + for index in sorted(options[kdesc].keys()): + self._installerOptions[desc].append(options[kdesc][index]) + + def onInstallationEnd( + self, result: mobase.InstallResult, new_mod: Optional[mobase.IModInterface] + ): + if ( + result != mobase.InstallResult.SUCCESS + or not self._installerUsed + or not new_mod + ): + return + + new_mod.clearPluginSettings(self.name()) + for i, desc in enumerate(self._installerOptions): + new_mod.setPluginSetting(self.name(), f"select{i}-description", desc) + for iopt, opt in enumerate(self._installerOptions[desc]): + new_mod.setPluginSetting(self.name(), f"select{i}-option{iopt}", opt) + + def _hasFomodInstaller(self) -> bool: + # do not consider the NCC installer + return self._organizer.isPluginEnabled("Fomod Installer") + + def _hasOmodInstaller(self) -> bool: + return self._organizer.isPluginEnabled("Omod Installer") + + def _getWizardArchiveBase( + self, tree: mobase.IFileTree, data_name: str, checker: mobase.ModDataChecker + ) -> Optional[mobase.IFileTree]: + """ + Try to find the folder containing wizard.txt. + + Args: + tree: Tree to look the data folder in. + data_name: Name of the data folder (e.g., "data" for Bethesda games). + checker: Checker to use to check if a tree is a data folder. + + Returns: + The tree corresponding to the folder containing wizard.txt, or None. + """ + + entry = tree.find("wizard.txt", mobase.FileTreeEntry.FILE) + + if entry: + return tree + + if len(tree) == 1 and isinstance((root := tree[0]), mobase.IFileTree): + return self._getWizardArchiveBase(root, data_name, checker) + + return None + + def _getEntriesToExtract( + self, + tree: mobase.IFileTree, + extensions: Sequence[str] = ["png", "jpg", "jpeg", "gif", "bmp", "ini"], + ) -> list[mobase.FileTreeEntry]: + """ + Retrieve all the entries to extract from the given tree. + + Args: + tree: The tree. + extensions: The extensions of files. + + Returns: + A list of entries corresponding to files with the given extensions. + """ + entries: list[mobase.FileTreeEntry] = [] + + def fn(path: str, entry: mobase.FileTreeEntry): + if entry.isFile() and entry.hasSuffix(extensions): + entries.append(entry) + return mobase.IFileTree.CONTINUE + + tree.walk(fn) + + return entries + + def isArchiveSupported(self, tree: mobase.IFileTree) -> bool: + """ + Check if the given file-tree (from the archive) can be installed by this + installer. + + Args: + tree: The tree to check. + + Returns: + True if the file-tree can be installed, false otherwise. + """ + + # retrieve the name of the "data" folder + data_name = self._organizer.managedGame().dataDirectory().dirName() + + # retrieve the mod-data-checker + checker = self._organizer.gameFeatures().gameFeature(mobase.ModDataChecker) + + # retrieve the base + base = self._getWizardArchiveBase(tree, data_name, checker) + + if not base: + return False + + # check FOMOD for priority + fomod = base.exists("fomod/ModuleConfig.xml") + if ( + fomod + and self._hasFomodInstaller() + and self._organizer.pluginSetting(self.name(), "prefer_fomod") + ): + return False + + # TODO: Check OMOD? + + return True + + def install( + self, + name: mobase.GuessedString, + tree: mobase.IFileTree, + version: str, + nexus_id: int, + ) -> Union[mobase.InstallResult, mobase.IFileTree]: + """ + Perform the actual installation. + + Args: + name: The "name" of the mod. This can be updated to change the name of the + mod. + tree: The original archive tree. + version: The original version of the mod. + nexus_id: The original ID of the mod. + + Returns: We either return the modified file-tree (if the installation was + successful), or a InstallResult otherwise. + + Note: It is also possible to return a tuple (InstallResult, IFileTree, str, int) + containing where the two last members correspond to the new version and ID + of the mod, in case those were updated by the installer. + """ + + # retrieve the name of the "data" folder + data_name = self._organizer.managedGame().dataDirectory().dirName() + + # retrieve the mod-data-checker + checker = self._organizer.gameFeatures().gameFeature(mobase.ModDataChecker) + + # retrieve the "base" folder + base = self._getWizardArchiveBase(tree, data_name, checker) + if not base or not checker: + return mobase.InstallResult.NOT_ATTEMPTED + + wizard = base.find("wizard.txt") + if wizard is None: + return mobase.InstallResult.NOT_ATTEMPTED + + to_extract = self._getEntriesToExtract(tree) + + # extract the script + paths = self._manager().extractFiles([wizard] + to_extract, silent=False) + if len(paths) != len(to_extract) + 1: + return mobase.InstallResult.FAILED + + interpreter = make_interpreter(base, self._organizer) + + script = paths[0] + + dialog = WizardInstallerDialog( + self._organizer, + interpreter, + interpreter.make_top_level_context(Path(script), WizardRunnerState()), + name, + { + Path(entry.path()): Path(path) + for entry, path in zip(to_extract, paths[1:], strict=True) + if not path.endswith(".ini") + }, + self._installerOptions, + self._parentWidget(), + ) + + dialog.scriptButtonClicked.connect( # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue] + lambda: os.startfile(script) + ) + + # unlike the official installer, we do not have a "silent" setting, but it is + # really simple to add it + if dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted: + # we update the name with the user specified one + name.update(dialog.name(), mobase.GuessQuality.USER) + + # create the tree with all the sub-packages + new_tree = tree.createOrphanTree() + + for subpackage in dialog.subpackages(): + entry = base.find(subpackage) + + # should never happens since we fetch the subpackage for the archive + if not entry or not isinstance(entry, mobase.IFileTree): + print( + f"SubPackage {subpackage} not found in the archive.", + file=sys.stderr, + ) + continue + + new_tree.merge(entry) + + # handle renames + for original, new in dialog.renames().items(): + # entry should be at the root + entry = new_tree.find(original) + + if not entry: + print(f"Plugin {original} not found, cannot rename.") + continue + + new_tree.move(entry, new) + + # move not selected plugins to optional + for plugin, enabled in dialog.plugins().items(): + if not enabled: + entry = new_tree.find(plugin) + if not entry: + continue # silently fail since the plugin should be disabled + new_tree.addDirectory("optional").insert(entry) + + # TODO: INI Tweaks: + alltweaks = dialog.tweaks() + + for filename, tweaks in alltweaks.items(): + # find the original file (if any) + o_entry = new_tree.find(filename) + o_filename: Optional[str] = None + if o_entry: + # find the filepath from the list of extracted files + index = to_extract.index(o_entry) + + # +1 because the first one is the script + o_filename = paths[index + 1] + + # if the file existed before, we keep the new one at the same place + if o_entry or Path(filename).parts[0].lower() == "ini tweaks": + entry = new_tree.addFile(filename, replace_if_exists=True) + + # otherwise we create it in INI Tweaks + else: + entry = new_tree.addFile( + os.path.join("INI Tweaks", filename), replace_if_exists=True + ) + + filepath = self._manager().createFile(entry) + + if not o_filename: + data = make_ini_tweaks(tweaks) + else: + data = merge_ini_tweaks(tweaks, Path(o_filename)) + + with open(filepath, "w") as fp: + fp.write(data) + + # mark stuff for saving + self._installerUsed = True + self._installerOptions = dict(dialog.selectedOptions()) + + return new_tree + + # if user requested a manual installation, we update the name (to keep it + # in the manual installation dialog) and just notify the installation manager + elif dialog.isManualRequested(): + name.update(dialog.name(), mobase.GuessQuality.USER) + return mobase.InstallResult.MANUAL_REQUESTED + + # if user canceled, we simply notify the installation manager + else: + return mobase.InstallResult.CANCELED + + def tr(self, value: str) -> str: + # we need this to translate string in Python. Check the common documentation + # for more details + return QApplication.translate("WizardInstaller", value) diff --git a/libs/installer_wizard/src/installer_wizard_en.ts b/libs/installer_wizard/src/installer_wizard_en.ts new file mode 100644 index 0000000..0b1bd68 --- /dev/null +++ b/libs/installer_wizard/src/installer_wizard_en.ts @@ -0,0 +1,156 @@ + + + + + WizardInstaller + + + BAIN Wizard Installer + + + + + Installer for BAIN archive containing wizard scripts. + + + + + WizardInstallerComplete + + + The installer script has finished. The following sub-packages and plugins will be installed. + + + + + Sub-Packages: + + + + + Plugins: + + + + + INI Tweaks: + + + + + Notes: + + + + + WizardInstallerDialog + + + + Next + + + + + Install anyway + + + + + Install + + + + + BAIN Wizard Installer + + + + + Name + + + + + Manual + + + + + Script + + + + + Back + + + + + Cancel + + + + + WizardInstallerPage + + + Options: + + + + + Description: + + + + + WizardInstallerRequires + + + <html><head/><body><p><span style=" font-weight:600;">Warning:</span> The following version requirements are not met for using this installer.</p></body></html> + + + + + Requirements + + + + + Graphics Extender + + + + + Wrye Bash + + + + + Need + + + + + Game + + + + + + N/A + + + + + Have + + + + + Script Extender + + + + diff --git a/libs/installer_wizard/src/runner.py b/libs/installer_wizard/src/runner.py new file mode 100644 index 0000000..e098e11 --- /dev/null +++ b/libs/installer_wizard/src/runner.py @@ -0,0 +1,189 @@ +from __future__ import annotations + +import sys +from pathlib import Path +from typing import Any, Iterable, List, Optional + +from wizard.interpreter import WizardInterpreter +from wizard.manager import ManagerModInterface +from wizard.severity import SeverityContext +from wizard.utils import make_runner_context_factory +from wizard.value import SubPackage, SubPackages + +import mobase + + +class MO2SubPackage(SubPackage): + _tree: mobase.IFileTree + _files: List[str] + + def __init__(self, tree: mobase.IFileTree): + super().__init__(tree.name()) + self._tree = tree + + # we cannot perform lazy iteration on the tree in a Python way so we + # have to list the files + self._files = [] + + def fn(folder: str, entry: mobase.FileTreeEntry) -> mobase.IFileTree.WalkReturn: + self._files.append(entry.path()) + return mobase.IFileTree.CONTINUE + + self._tree.walk(fn) + + @property + def files(self) -> Iterable[str]: + return self._files + + +class MO2SeverityContext(SeverityContext): + _organizer: mobase.IOrganizer + + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self._organizer = organizer + + def warning(self, text: str): + print(text, file=sys.stderr) + + +class MO2ManagerModInterface(ManagerModInterface): + _organizer: mobase.IOrganizer + _game: mobase.IPluginGame + _subpackages: SubPackages + + def __init__(self, tree: mobase.IFileTree, organizer: mobase.IOrganizer): + self._organizer = organizer + self._game = organizer.managedGame() + + checker = self._organizer.gameFeatures().gameFeature(mobase.ModDataChecker) + + # read the sub-packages + self._subpackages = SubPackages() + for entry in tree: + if isinstance(entry, mobase.IFileTree): + if checker: + if checker.dataLooksValid(entry) == mobase.ModDataChecker.VALID: + self._subpackages.append(MO2SubPackage(entry)) + continue + + # add entry with INI tweaks + if entry.exists("INI Tweaks") or entry.exists("INI"): + self._subpackages.append(MO2SubPackage(entry)) + continue + + # we add folder with format "XXX Docs" where "XXX" is a number + parts = entry.name().split() + if ( + len(parts) >= 2 + and parts[0].isdigit() + and parts[1].lower().startswith("doc") + ): + self._subpackages.append(MO2SubPackage(entry)) + + @property + def subpackages(self) -> SubPackages: + return self._subpackages + + def compareGameVersion(self, version: str) -> int: + v1 = mobase.VersionInfo(version) + v2 = mobase.VersionInfo(self._game.gameVersion()) + if v1 < v2: + return 1 + elif v1 > v2: + return -1 + else: + return 0 + + def compareSEVersion(self, version: str) -> int: + se = self._organizer.gameFeatures().gameFeature(mobase.ScriptExtender) + if not se: + return 1 + v1 = mobase.VersionInfo(version) + v2 = mobase.VersionInfo(se.getExtenderVersion()) + if v1 < v2: + return 1 + elif v1 > v2: + return -1 + else: + return 0 + + def compareGEVersion(self, version: str) -> int: + # cannot do th is in MO2 + return 1 + + def compareWBVersion(self, version: str) -> int: + # cannot do this in MO2 + return 1 + + def _resolve(self, filepath: str) -> Optional[Path]: + """ + Resolve the given filepath. + + Args: + filepath: The path to resolve. + + Returns: + The path to the given file on the disk, or one of the file mapping + to it in the VFS, or None if the file does not exists. + """ + # TODO: This does not handle weird path that go back (..) and then in data + # again, e.g. ../data/xxx.esp. + path: Optional[Path] + if filepath.startswith(".."): + path = Path(self._game.dataDirectory().absoluteFilePath(filepath)) + if not path.exists(): + path = None + else: + path = Path(filepath) + parent = path.parent.as_posix() + if parent == ".": + parent = "" + + files = self._organizer.findFiles(parent, "*" + path.name) + if files: + path = Path(files[0]) + else: + path = None + + return path + + def dataFileExists(self, *filepaths: str) -> bool: + return all(self._resolve(path) for path in filepaths) + + def getPluginLoadOrder(self, filename: str, fallback: int = -1) -> int: + return self._organizer.pluginList().loadOrder(filename) + + def getPluginStatus(self, filename: str) -> int: + state = self._organizer.pluginList().state(filename) + + if state == mobase.PluginState.ACTIVE: + return 2 + if state == mobase.PluginState.INACTIVE: + return 0 # Or 1? + return -1 + + def getFilename(self, path: str) -> str: + path_ = self._resolve(path) + if path_: + if path_.is_file(): + return path_.name + return "" + + def getFolder(self, path: str) -> str: + path_ = self._resolve(path) + if path_: + if path_.is_dir(): + return path_.name + return "" + + +def make_interpreter( + base: mobase.IFileTree, organizer: mobase.IOrganizer +) -> WizardInterpreter[Any]: + manager = MO2ManagerModInterface(base, organizer) + severity = MO2SeverityContext(organizer) + + factory = make_runner_context_factory(manager.subpackages, manager, severity) + + return WizardInterpreter(factory) diff --git a/libs/installer_wizard/src/ui/wizardinstallercomplete.ui b/libs/installer_wizard/src/ui/wizardinstallercomplete.ui new file mode 100644 index 0000000..e5357dd --- /dev/null +++ b/libs/installer_wizard/src/ui/wizardinstallercomplete.ui @@ -0,0 +1,271 @@ + + + WizardInstallerComplete + + + + 0 + 0 + 652 + 476 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 0 + 0 + + + + The installer script has finished. The following sub-packages and plugins will be installed. + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + + 0 + 15 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + Sub-Packages: + + + true + + + + + + + + + + + + + + Plugins: + + + true + + + + + + + + + + + + + + + + + + 0 + 10 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 0 + 0 + + + + 0 + + + INI Tweaks: + + + true + + + + + + + + + + + + true + + + + + + + + + + + + + 0 + 10 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Notes: + + + true + + + + + + + + 0 + 0 + + + + true + + + + + + + + + + + + + + + + + + + diff --git a/libs/installer_wizard/src/ui/wizardinstallerdialog.ui b/libs/installer_wizard/src/ui/wizardinstallerdialog.ui new file mode 100644 index 0000000..4ae2565 --- /dev/null +++ b/libs/installer_wizard/src/ui/wizardinstallerdialog.ui @@ -0,0 +1,128 @@ + + + WizardInstallerDialog + + + + 0 + 0 + 769 + 477 + + + + BAIN Wizard Installer + + + + + + 0 + + + + + Name + + + + + + + + 0 + 0 + + + + true + + + + + + + + + -1 + + + + + + + + + Manual + + + false + + + + + + + Script + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Back + + + false + + + + + + + Next + + + + + + + Cancel + + + false + + + false + + + false + + + + + + + + + + diff --git a/libs/installer_wizard/src/ui/wizardinstallererror.ui b/libs/installer_wizard/src/ui/wizardinstallererror.ui new file mode 100644 index 0000000..d364001 --- /dev/null +++ b/libs/installer_wizard/src/ui/wizardinstallererror.ui @@ -0,0 +1,103 @@ + + + WizardInstallerError + + + + 0 + 0 + 652 + 476 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 0 + 0 + + + + + 32 + 32 + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + true + + + + + + + + + true + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + + + + + + + diff --git a/libs/installer_wizard/src/ui/wizardinstallerpage.ui b/libs/installer_wizard/src/ui/wizardinstallerpage.ui new file mode 100644 index 0000000..2e31c83 --- /dev/null +++ b/libs/installer_wizard/src/ui/wizardinstallerpage.ui @@ -0,0 +1,171 @@ + + + WizardInstallerPage + + + + 0 + 0 + 652 + 476 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 6 + + + + + true + + + + 0 + 0 + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Plain + + + 1 + + + 0 + + + + 0 + + + 2 + + + 0 + + + 2 + + + + + 6 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + + + true + + + 0 + + + + + + + + + + + + Options: + + + + + + + + + QListView::Adjust + + + + + + + + 0 + 0 + + + + + + + false + + + Qt::AlignCenter + + + + + + + + + Description: + + + + + + + QFrame::WinPanel + + + true + + + + + + + + + + diff --git a/libs/installer_wizard/src/ui/wizardinstallerrequires.ui b/libs/installer_wizard/src/ui/wizardinstallerrequires.ui new file mode 100644 index 0000000..63bd607 --- /dev/null +++ b/libs/installer_wizard/src/ui/wizardinstallerrequires.ui @@ -0,0 +1,220 @@ + + + WizardInstallerRequires + + + + 0 + 0 + 652 + 476 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + <html><head/><body><p><span style=" font-weight:600;">Warning:</span> The following version requirements are not met for using this installer.</p></body></html> + + + 10 + + + + + + + Requirements + + + + + + + + Graphics Extender + + + true + + + + + + + + + + + + + + + + + + + + + Wrye Bash + + + true + + + + + + + Need + + + true + + + + + + + + + + + + + + Game + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + N/A + + + + + + + Have + + + true + + + + + + + N/A + + + + + + + + + + + + + + Script Extender + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 20 + 40 + + + + + + + + + + + diff --git a/libs/installer_wizard/src/utils.py b/libs/installer_wizard/src/utils.py new file mode 100644 index 0000000..da85c48 --- /dev/null +++ b/libs/installer_wizard/src/utils.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import re +from collections.abc import Sequence +from pathlib import Path +from typing import cast + +from wizard.tweaks import WizardINISetting, WizardINISettingEdit + + +def make_obscript_ini_tweaks(tweaks: Sequence[WizardINISetting]) -> str: + lines: list[str] = [] + + for tweak in tweaks: + if not isinstance(tweak, WizardINISettingEdit): + continue + line = f"{tweak.section} {tweak.setting} to {tweak.value}" + if tweak.comment: + line += f" ; {tweak.comment}" + + lines.append(line) + + return "\n".join(["; Generated by Mod Organizer 2 via Wizard"] + sorted(lines)) + + +def make_standard_ini_tweaks(tweaks: Sequence[WizardINISetting]) -> str: + # group Tweaks per section + sections: dict[str, list[WizardINISetting]] = {m.section: [] for m in tweaks} + for m in tweaks: + sections[m.section].append(m) + + lines: list[str] = [] + + for k in sorted(sections): + s_tweaks = sorted(sections[k], key=lambda m: m.setting) + lines.append(f"[{k}]") + for tw in s_tweaks: + if isinstance(tw, WizardINISettingEdit): + line = f"{tw.setting} = {tw.value}" + if tw.comment: + line += f" # {tw.comment}" + else: + line = f"# {tw.setting} - disabled" + lines.append(line) + lines.append("\n") + + return "\n".join(lines[:-1]) + + +def merge_standard_ini_tweaks(tweaks: Sequence[WizardINISetting], file: Path) -> str: + import sys + + print(f"Cannot merge INI Tweaks for {file.name}.", file=sys.stderr) + return make_standard_ini_tweaks(tweaks) + + +def merge_obscript_ini_tweaks(tweaks: Sequence[WizardINISetting], file: Path) -> str: + # this is from Wrye Bash (and the function is inspired from Wrye Bash) + reComment = re.compile(";.*", re.U) + reDeleted = re.compile("" r";-(\w.*?)$", re.U) + reSet = re.compile("" r"\s*set\s+(.+?)\s+to\s+(.*)", re.I | re.U) + reSetGS = re.compile("" r"\s*setGS\s+(.+?)\s+(.*)", re.I | re.U) + reSetNGS = re.compile("" r"\s*SetNumericGameSetting\s+(.+?)\s+(.*)", re.I | re.U) + + _regex_tuples = ( + (reSet, "set", "set {} to {}"), + (reSetGS, "setgs", "setGS {} {}"), + (reSetNGS, "setnumericgamesetting", "SetNumericGameSetting {} {}"), + ) + + def _parse_obse_line(line: str): + for regex, sectionKey, format_string in _regex_tuples: + match = regex.match(line) + if match: + return match, sectionKey, format_string + return None, None, None + + # read the original file + with open(file, "r") as fp: + olines = fp.readlines() + + # map setting name to value + settings: dict[str, dict[str, WizardINISettingEdit]] = {} + deleted: dict[str, dict[str, WizardINISetting]] = {} + for tweak in tweaks: + if isinstance(tweak, WizardINISettingEdit): + if tweak.section not in settings: + settings[tweak.section.lower()] = {} + settings[tweak.section.lower()][tweak.setting] = tweak + else: + if tweak.section not in deleted: + deleted[tweak.section.lower()] = {} + deleted[tweak.section.lower()][tweak.setting] = tweak + + # create the lines + lines: list[str] = [] + for line in olines: + line = line.rstrip() + maDeleted = reDeleted.match(line) + if maDeleted: + stripped = maDeleted.group(1) + else: + stripped = line + stripped = reComment.sub("", stripped).strip() + + match, section_key, format_string = _parse_obse_line(stripped) + + if match: + assert format_string is not None + setting = match.group(1) + if section_key in settings and setting in settings[section_key]: + value = settings[section_key][setting].value + line = format_string.format(setting, value) + comment = "" + if settings[section_key][setting].comment: + comment = cast(str, settings[section_key][setting].comment) + comment += " " + comment += f"(set by MO2 via Wizard, was {match.group(2)})" + line = f"{line} ; {comment}" + del settings[section_key][setting] + elif ( + not maDeleted + and section_key in deleted + and setting in deleted[section_key] + ): + line = f";-{line}" + + lines.append(line) + + for section in settings.values(): + line = "" + for setting in section.values(): + if setting.section.lower() == "set": + line = f"{setting.section} {setting.setting} to {setting.value}" + else: + line = f"{setting.section} {setting.setting} {setting.value}" + + if setting.comment: + comment = setting.comment + " (set by MO2 via Wizard)" + else: + comment = "(set by MO2 via Wizard)" + line = f"{line} ; {comment}" + + lines.append(line) + + return "\n".join(lines) + + +def make_ini_tweaks(tweaks: Sequence[WizardINISetting]) -> str: + is_set = [ + tw.section.lower() in ("set", "setgs", "setnumericgamesetting") for tw in tweaks + ] + + # assume there is no mix + if all(is_set): + return make_obscript_ini_tweaks(tweaks) + else: + return make_standard_ini_tweaks(tweaks) + + +def merge_ini_tweaks(tweaks: Sequence[WizardINISetting], file: Path) -> str: + is_set = [ + tw.section.lower() in ("set", "setgs", "setnumericgamesetting") for tw in tweaks + ] + + # assume there is no mix + if all(is_set): + return merge_obscript_ini_tweaks(tweaks, file) + else: + return merge_standard_ini_tweaks(tweaks, file) diff --git a/libs/installer_wizard/vcpkg.json b/libs/installer_wizard/vcpkg.json new file mode 100644 index 0000000..a27b7c5 --- /dev/null +++ b/libs/installer_wizard/vcpkg.json @@ -0,0 +1,17 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": [ + "mo2-cmake" + ] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "69db61b22147b14ad66fd05cf798d855f6d68c12" + } + } +} diff --git a/libs/libbsarch/.clang-format b/libs/libbsarch/.clang-format new file mode 100644 index 0000000..42a3a23 --- /dev/null +++ b/libs/libbsarch/.clang-format @@ -0,0 +1,117 @@ +--- +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - forever + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^> $GITHUB_ENV + + - uses: actions/checkout@v4 + + - run: cmake --preset vs2022-windows "-DCMAKE_INSTALL_PREFIX=install" + + # build both Debug and Release for package + - name: Build libbsarch + run: | + cmake --build vsbuild --config Debug --verbose + cmake --build vsbuild --config Release --verbose + + - name: Install libbsarch + run: | + cmake --install vsbuild --config Debug + cmake --install vsbuild --config Release + + - name: Upload libbsarch artifact + uses: actions/upload-artifact@master + with: + name: libbsarch + path: ./install + + publish: + if: github.ref_type == 'tag' + needs: build + runs-on: windows-2022 + permissions: + contents: write + steps: + - name: Download Artifact + uses: actions/download-artifact@master + with: + name: libbsarch + path: ./install + + - name: Create libbsarch archive + run: 7z a libbsarch_${{ github.ref_name }}.7z ./install/* + + - name: Publish Release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh release create --draft=false --notes="${{ github.ref_name }}" "${{ github.ref_name }}" ./libbsarch_${{ github.ref_name }}.7z diff --git a/libs/libbsarch/.github/workflows/linting.yml b/libs/libbsarch/.github/workflows/linting.yml new file mode 100644 index 0000000..12681a8 --- /dev/null +++ b/libs/libbsarch/.github/workflows/linting.yml @@ -0,0 +1,17 @@ +name: Lint libbsarch + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." + exclude-regex: "third-party" diff --git a/libs/libbsarch/.gitignore b/libs/libbsarch/.gitignore new file mode 100644 index 0000000..4c54425 --- /dev/null +++ b/libs/libbsarch/.gitignore @@ -0,0 +1,539 @@ + +# Created by https://www.gitignore.io/api/qt,c++,cmake,delphi,qtcreator,visualstudio +# Edit at https://www.gitignore.io/?templates=qt,c++,cmake,delphi,qtcreator,visualstudio + +### C++ ### +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +### CMake ### +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps + + +### CMake Patch ### +# External projects +*-prefix/ + +### Delphi ### +# Uncomment these types if you want even more clean repository. But be careful. +# It can make harm to an existing project source. Read explanations below. +# +# Resource files are binaries containing manifest, project icon and version info. +# They can not be viewed as text or compared by diff-tools. Consider replacing them with .rc files. +#*.res +# Type library file (binary). In old Delphi versions it should be stored. +# Since Delphi 2009 it is produced from .ridl file and can safely be ignored. +#*.tlb +# Diagram Portfolio file. Used by the diagram editor up to Delphi 7. +# Uncomment this if you are not using diagrams or use newer Delphi version. +#*.ddp +# Visual LiveBindings file. Added in Delphi XE2. +# Uncomment this if you are not using LiveBindings Designer. +#*.vlb +# Deployment Manager configuration file for your project. Added in Delphi XE2. +# Uncomment this if it is not mobile development and you do not use remote debug feature. +#*.deployproj +# C++ object files produced when C/C++ Output file generation is configured. +# Uncomment this if you are not using external objects (zlib library for example). +#*.obj + +# Delphi compiler-generated binaries (safe to delete) +*.bpl +*.bpi +*.dcp +*.apk +*.drc +*.map +*.dres +*.rsm +*.tds +*.dcu +*.ocx + +# Delphi autogenerated files (duplicated info) +*.cfg +# *.hpp +*Resource.rc + +# Delphi local files (user-specific info) +*.local +*.identcache +*.projdata +*.tvsconfig +*.dsk + +# Delphi history and backups +__history/ +__recovery/ +*.~* + +# Castalia statistics file (since XE7 Castalia is distributed with Delphi) +*.stat + +# Boss dependency manager vendor folder https://github.com/HashLoad/boss +modules/ + +### Qt ### +# C++ objects and libs + +# Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +*.qmlc +*.jsc +Makefile* +*build-* +vsbuild + +# Qt unit tests +target_wrapper.* + +# QtCreator +*.autosave + +# QtCreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCreator CMake +CMakeLists.txt.user* + +# QtCreator 4.8< compilation database + +# QtCreator local machine specific files for imported projects +*creator.user* + +### QtCreator ### +# gitignore for Qt Creator like IDE for pure C/C++ project without Qt +# Reference: http://doc.qt.io/qtcreator/creator-project-generic.html + + + +# Qt Creator autogenerated files + + +# A listing of all the files included in the project +*.files + +# Include directories +*.includes + +# Project configuration settings like predefined Macros +*.config + +# Qt Creator settings +*.creator + +# User project settings +*.creator.user* + +# Qt Creator backups + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.iobj +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# End of https://www.gitignore.io/api/qt,c++,cmake,delphi,qtcreator,visualstudio + +install +examples/build +examples/vsbuild +examples/test_ba1 +examples/test_ba2 diff --git a/libs/libbsarch/.pre-commit-config.yaml b/libs/libbsarch/.pre-commit-config.yaml new file mode 100644 index 0000000..3103a1f --- /dev/null +++ b/libs/libbsarch/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v19.1.5 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/libbsarch/CMakeLists.txt b/libs/libbsarch/CMakeLists.txt new file mode 100644 index 0000000..d3e994e --- /dev/null +++ b/libs/libbsarch/CMakeLists.txt @@ -0,0 +1,144 @@ +cmake_minimum_required(VERSION 3.16) + +include(CMakePackageConfigHelpers) + +project(libbsarch CXX) + +find_package(mo2-dds-header CONFIG REQUIRED) + +get_target_property(SRCS mo2::dds-header SOURCES) + +# libbsarch +add_library(libbsarch SHARED) +if(WIN32) + target_sources(libbsarch + PRIVATE + src/libbsarch.cpp + src/libbsarch.def + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include + FILES ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/libbsarch.h + ) +else() + target_sources(libbsarch + PRIVATE + src/libbsarch.cpp + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include + FILES ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/libbsarch.h + ) + # Set default symbol visibility to hidden, export only marked symbols + target_compile_options(libbsarch PRIVATE -fvisibility=hidden) +endif() +target_include_directories(libbsarch PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch) +set_target_properties(libbsarch PROPERTIES DEBUG_POSTFIX "d" CXX_STANDARD 11) +target_link_libraries(libbsarch PUBLIC mo2::dds-header) +target_compile_definitions(libbsarch PRIVATE BSARCH_DLL_EXPORT) + +# preprocessor definitions +if(MSVC) + target_compile_options(libbsarch PRIVATE + /W3 /EHsc /MP + $<$:/MDd /Od> + $<$:/Zi> + ) +endif() + + +# libbsarch_OOP +add_library(libbsarch_OOP STATIC) + +target_sources(libbsarch_OOP + PRIVATE + src/bs_archive_entries.cpp + src/utils/string_convert.cpp + src/utils/convertible_ostream.cpp + src/utils/convertible_string.cpp + src/bs_archive_auto.cpp + src/bs_archive.cpp + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/base_types.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/bs_archive_auto.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/bs_archive.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/bs_archive_entries.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/libbsarch.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/libbsarch.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/utils/convertible_string.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/utils/convertible_ostream.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/utils/string_convert.hpp +) +target_include_directories(libbsarch_OOP PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/utils) +target_link_libraries(libbsarch_OOP PUBLIC mo2::dds-header) + +set_target_properties(libbsarch_OOP PROPERTIES + DEBUG_POSTFIX "d" + CXX_STANDARD 17 +) + +if (MSVC) + target_compile_options(libbsarch_OOP PRIVATE /MP) +endif() + +# target_link_libraries(libbsarch_OOP PUBLIC libbsarch) + +# install + +# finding 32 or 64bits +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + # 64 bits + set(lib_delphi "${CMAKE_CURRENT_SOURCE_DIR}/delphi/lib64") + message("libbsarch: using 64bits DLL") +elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) + # 32 bits + set(lib_delphi "${CMAKE_CURRENT_SOURCE_DIR}/delphi/lib") + message("libbsarch: using 32bits DLL") +endif() + +install(TARGETS libbsarch libbsarch_OOP EXPORT libbsarchTargets FILE_SET HEADERS) +if(WIN32) + install(FILES $ DESTINATION pdb OPTIONAL) +endif() +install(EXPORT libbsarchTargets + FILE mo2-libbsarch-targets.cmake + NAMESPACE mo2:: + DESTINATION lib/cmake/mo2-libbsarch +) + +if(WIN32) + # install the true DLLs (Windows only - Delphi-built BSArch) + install(FILES ${lib_delphi}/libbsarch.dll DESTINATION bin CONFIGURATIONS Release RelWithDebInfo MinSizeRel) + install(FILES ${lib_delphi}/libbsarchd.dll DESTINATION bin CONFIGURATIONS Debug) +endif() + +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/mo2-libbsarch-config.cmake" + INSTALL_DESTINATION "lib/cmake/mo2-libbsarch" + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO +) + + +include(${CMAKE_CURRENT_SOURCE_DIR}/Version.cmake) +set(LIBBSARCH_VERSION + ${LIBBSARCH_MAJOR_VERSION}.${LIBBSARCH_MINOR_VERSION}.${LIBBSARCH_PATCH_VERSION}) + + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/mo2-libbsarch-config-version.cmake" + VERSION "${LIBBSARCH_VERSION}" + COMPATIBILITY AnyNewerVersion + ARCH_INDEPENDENT +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/mo2-libbsarch-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/mo2-libbsarch-config-version.cmake + DESTINATION lib/cmake/mo2-libbsarch +) diff --git a/libs/libbsarch/CMakePresets.json b/libs/libbsarch/CMakePresets.json new file mode 100644 index 0000000..acc6d97 --- /dev/null +++ b/libs/libbsarch/CMakePresets.json @@ -0,0 +1,17 @@ +{ + "configurePresets": [ + { + "binaryDir": "${sourceDir}/vsbuild", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "generator": "Visual Studio 17 2022", + "name": "vs2022-windows" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/libbsarch/LICENSE b/libs/libbsarch/LICENSE new file mode 100644 index 0000000..c2799e0 --- /dev/null +++ b/libs/libbsarch/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Deorder + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/libbsarch/README.md b/libs/libbsarch/README.md new file mode 100644 index 0000000..671b6ff --- /dev/null +++ b/libs/libbsarch/README.md @@ -0,0 +1,25 @@ +# BSArchive DLL and C++ bindings + +BSArchive Dynamic Link Library and C++ bindings + +## How to build + +1) Create a `XEditPath` environment variable and set it to the directory containing the XEdit code +2) Build the DLL in Delphi by opening the `libbsarch.dproj` project +3) Build the LIB in Visual Studio by opening the `libbsarch.sln` project + +## How to use + +With CMake: +Use ``add_subdirectory`` and ``target_link_library`` +Then ``#include "libbsarch.h"`` for the C wrapper, or ``#include "bs_archive_auto"`` for the C++ wrapper. +See the example for code use. + +Without CMake: +Use `libbsarch.dll` (provided in delphi/lib), `libbsarch.lib` and `libbsarch.h` in your project. + +## Credits + +The original BSArchive can be found at: https://github.com/TES5Edit/TES5Edit/tree/dev/Tools/BSArchive +The version in this project had been modified for better compatibility with C/C++ and +allow the users to allocate their own memory in some cases. diff --git a/libs/libbsarch/Version.cmake b/libs/libbsarch/Version.cmake new file mode 100644 index 0000000..59f33b6 --- /dev/null +++ b/libs/libbsarch/Version.cmake @@ -0,0 +1,3 @@ +set(LIBBSARCH_MAJOR_VERSION 0) +set(LIBBSARCH_MINOR_VERSION 1) +set(LIBBSARCH_PATCH_VERSION 2) diff --git a/libs/libbsarch/cmake/config.cmake.in b/libs/libbsarch/cmake/config.cmake.in new file mode 100644 index 0000000..8bb228f --- /dev/null +++ b/libs/libbsarch/cmake/config.cmake.in @@ -0,0 +1,37 @@ +@PACKAGE_INIT@ + +include ( "${CMAKE_CURRENT_LIST_DIR}/mo2-libbsarch-targets.cmake" ) + +target_link_libraries(mo2::libbsarch INTERFACE mo2::libbsarch_OOP) + +# libbsarch is usually be build in Debug or Release/RelWithDebInfo, so we need to map +# missing configurations to avoid issue with CMP0111 +get_target_property(_LIBBSARCH_CONFIGURATIONS mo2::libbsarch IMPORTED_CONFIGURATIONS) +list(FIND _LIBBSARCH_CONFIGURATIONS "RELEASE" _LIBBSARCH_HAS_RELEASE) +if (_LIBBSARCH_HAS_RELEASE EQUAL -1) + set_target_properties(mo2::libbsarch PROPERTIES + MAP_IMPORTED_CONFIG_MINSIZEREL RelWithDebInfo + MAP_IMPORTED_CONFIG_RELEASE RelWithDebInfo + ) + set_property(TARGET mo2::libbsarch APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL) + set_property(TARGET mo2::libbsarch APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) + set_target_properties(mo2::libbsarch_OOP PROPERTIES + MAP_IMPORTED_CONFIG_MINSIZEREL RelWithDebInfo + MAP_IMPORTED_CONFIG_RELEASE RelWithDebInfo + ) + set_property(TARGET mo2::libbsarch_OOP APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL) + set_property(TARGET mo2::libbsarch_OOP APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) +else() + set_target_properties(mo2::libbsarch PROPERTIES + MAP_IMPORTED_CONFIG_MINSIZEREL Release + MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release + ) + set_property(TARGET mo2::libbsarch APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL) + set_property(TARGET mo2::libbsarch APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO) + set_target_properties(mo2::libbsarch_OOP PROPERTIES + MAP_IMPORTED_CONFIG_MINSIZEREL Release + MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release + ) + set_property(TARGET mo2::libbsarch_OOP APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL) + set_property(TARGET mo2::libbsarch_OOP APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO) +endif() diff --git a/libs/libbsarch/delphi/libbsarch.dpr b/libs/libbsarch/delphi/libbsarch.dpr new file mode 100644 index 0000000..34c0e35 --- /dev/null +++ b/libs/libbsarch/delphi/libbsarch.dpr @@ -0,0 +1,412 @@ +library libbsarch; + +uses + Math, + Classes, + Types, + SysUtils, + wbStreams, + wbBSArchive; + +type + TwbBSResultCode = ( + BSA_RESULT_NONE = 0, + BSA_RESULT_EXCEPTION = -1 + ); + TwbBSResultMessage = packed record + code: ShortInt; + text: array[0..1023] of WideChar; + end; + TwbBSResultMessageBuffer = packed record + buffer: TwbBSResultBuffer; + message: TwbBSResultMessage; + end; + +{Shared} + +function string_to_cstring( + aString: String; + const aStringBufferSize: Cardinal; + const aStringBuffer: PChar +): Integer; stdcall; +begin + Result := Min(aStringBufferSize, Length(AString)); + if (aStringBuffer <> nil) and (Result > 0) then + Move(AString[1], aStringBuffer^, Result * SizeOf(WideChar)); +end; + +function string_to_cstring_end( + aString: String; + const aStringBufferSize: Cardinal; + const aStringBuffer: PChar +): Integer; stdcall; +begin + Result := Min(aStringBufferSize - 1, Length(AString)); + if (aStringBuffer <> nil) and (Result > 0) then + begin + Move(AString[1], aStringBuffer^, Result * SizeOf(WideChar)); + aStringBuffer[Result] := #0; + end; +end; + +procedure exception_handler(E: Exception; var Result: TwbBSResultMessage); stdcall; +begin + Result.code := ShortInt(BSA_RESULT_EXCEPTION); + string_to_cstring_end(E.Message, Length(Result.text), Result.text); +end; + +procedure buffer_exception_handler(E: Exception; var Result: TwbBSResultMessageBuffer); stdcall; +begin + Result.buffer.size := 0; + Result.buffer.data := nil; + Result.message.code := ShortInt(BSA_RESULT_EXCEPTION); + string_to_cstring_end(E.Message, Length(Result.message.text), Result.message.text); +end; + +{BSA File List} + +function bsa_entry_list_create:Pointer; stdcall; +begin + try + Result := TStringList.Create; + except + Result := nil; + end; +end; + +function bsa_entry_list_free(obj: Pointer): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TStringList(obj).Clear(); + TStringList(obj).free; + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_entry_list_count(obj: Pointer): Integer; stdcall; +begin + try + Result := TStringList(obj).Count; + except + Result := Ord(BSA_RESULT_EXCEPTION); + end; +end; + +function bsa_entry_list_add(obj: Pointer; const aEntryString: PChar): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TStringList(obj).Add(String(aEntryString)); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_entry_list_get(obj: Pointer; const aIndex: Cardinal; const aStringBufferSize: Cardinal; const aStringBuffer: PChar): Integer; stdcall; +begin + try + Result := string_to_cstring_end(TStringList(obj).Strings[aIndex], aStringBufferSize, aStringBuffer); + except + Result := Integer(BSA_RESULT_EXCEPTION); + end; +end; + +{BSArchive} + +function bsa_create:Pointer; stdcall; +begin + try + Result := TwbBSArchive.Create; + except + Result := nil; + end; +end; + +function bsa_free(obj: Pointer): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).free; + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_load_from_file(obj: Pointer; const aFilePath: PChar): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).LoadFromFile(String(aFilePath)); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_create_archive(obj: Pointer; const aFilePath: PChar; aType: TBSArchiveType; aFileList: TStringList): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).CreateArchive(String(aFilePath), aType, aFileList); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_save(obj: Pointer): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).Save; + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_add_file_from_disk_root(obj: Pointer; const aRootDir, aSourcePath: PChar): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).AddFileDiskRoot(String(aRootDir), String(aSourcePath)); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_add_file_from_disk(obj: Pointer; const aFilePath, aSourcePath: PChar): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).AddFileDisk(String(aFilePath), String(aSourcePath)); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_add_file_from_memory(obj: Pointer; const aFilePath: PChar; const aSize: Cardinal; const aData: PByte): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).AddFileData(String(aFilePath), aSize, aData); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_find_file_record(obj: Pointer; const aFilePath: PChar): Pointer; stdcall; +begin + try + Result := TwbBSArchive(obj).FindFileRecord(String(aFilePath)); + except + Result := nil + end; +end; + +function bsa_extract_file_data_by_record(obj: Pointer; aFileRecord: Pointer): TwbBSResultMessageBuffer; stdcall; +begin + Result.message.code := Ord(BSA_RESULT_NONE); + try + Result.buffer := TwbBSArchive(obj).ExtractFileData(aFileRecord); + except + on E: Exception do + buffer_exception_handler(E, Result); + end; +end; + +function bsa_extract_file_data_by_filename(obj: Pointer; const aFilePath: PChar): TwbBSResultMessageBuffer; stdcall; +begin + Result.message.code := Ord(BSA_RESULT_NONE); + try + Result.buffer := TwbBSArchive(obj).ExtractFileData(String(aFilePath)); + except + on E: Exception do + buffer_exception_handler(E, Result); + end; +end; + +function bsa_file_data_free(obj: Pointer; fileDataResult: TwbBSResultMessageBuffer): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).ReleaseFileData(fileDataResult.buffer); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_extract_file(obj: Pointer; const aFilePath, aSaveAs: PChar): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).ExtractFile(String(aFilePath), String(aSaveAs)); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_iterate_files(obj: Pointer; aProc: TBSFileIterationProc; aContext: Pointer): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).IterateFiles(aProc, aContext); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_file_exists(obj: Pointer; const aFilePath: string): Boolean; stdcall; +begin + try + Result := TwbBSArchive(obj).FileExists(aFilePath); + except + Result := False; + end; +end; + +function bsa_get_resource_list(obj: Pointer; const aEntryResultList: TStringList; aFolder: PChar): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).ResourceList(aEntryResultList, String(aFolder)); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_close(obj: Pointer): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).Close; + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_filename_get(obj: Pointer; const aStringBufferSize: Cardinal; const aStringBuffer: PChar): Cardinal; stdcall; +begin + Result := string_to_cstring_end(TwbBSArchive(obj).FileName, aStringBufferSize, aStringBuffer); +end; + +function bsa_archive_type_get(obj: Pointer): TBSArchiveType; stdcall; +begin + Result := TwbBSArchive(obj).ArchiveType; +end; + +function bsa_version_get(obj: Pointer): Cardinal; stdcall; +begin + Result := TwbBSArchive(obj).Version; +end; + +function bsa_format_name_get(obj: Pointer; const aStringBufferSize: Cardinal; const aStringBuffer: PChar): Cardinal; stdcall; +begin + Result := string_to_cstring_end(TwbBSArchive(obj).FormatName, aStringBufferSize, aStringBuffer); +end; + +function bsa_file_count_get(obj: Pointer): Cardinal; stdcall; +begin + Result := TwbBSArchive(obj).FileCount; +end; + +function bsa_archive_flags_get(obj: Pointer): Cardinal; stdcall; +begin + Result := TwbBSArchive(obj).ArchiveFlags; +end; + +procedure bsa_archive_flags_set(obj: Pointer; flags: Cardinal); stdcall; +begin + TwbBSArchive(obj).ArchiveFlags := flags; +end; + +function bsa_file_flags_get(obj: Pointer): Cardinal; stdcall; +begin + Result := TwbBSArchive(obj).FileFlags; +end; + +procedure bsa_file_flags_set(obj: Pointer; flags: Cardinal); stdcall; +begin + TwbBSArchive(obj).FileFlags := flags; +end; + +function bsa_compress_get(obj: Pointer): Boolean; stdcall; +begin + Result := TwbBSArchive(obj).Compress; +end; + +procedure bsa_compress_set(obj: Pointer; compress: Boolean); stdcall; +begin + TwbBSArchive(obj).Compress := compress; +end; + +function bsa_share_data_get(obj: Pointer): Boolean; stdcall; +begin + Result := TwbBSArchive(obj).ShareData; +end; + +procedure bsa_share_data_set(obj: Pointer; shareData: Boolean); stdcall; +begin + TwbBSArchive(obj).ShareData := shareData; +end; + +procedure bsa_file_dds_info_callback_set(obj: Pointer; aProc: TBSFileDDSInfoProc; aContext: Pointer); stdcall; +begin + TwbBSArchive(obj).DDSInfoProc := aProc; + TwbBSArchive(obj).DDSInfoProcContext := aContext; +end; + +exports + bsa_entry_list_create, + bsa_entry_list_free, + bsa_entry_list_count, + bsa_entry_list_add, + bsa_entry_list_get, + + bsa_filename_get, + bsa_archive_type_get, + bsa_version_get, + bsa_format_name_get, + bsa_file_count_get, + bsa_archive_flags_get, + bsa_archive_flags_set, + bsa_file_flags_get, + bsa_file_flags_set, + bsa_compress_get, + bsa_compress_set, + bsa_share_data_get, + bsa_share_data_set, + bsa_file_dds_info_callback_set, + + bsa_close, + bsa_get_resource_list, + bsa_file_exists, + bsa_iterate_files, + bsa_extract_file, + bsa_file_data_free, + bsa_extract_file_data_by_filename, + bsa_extract_file_data_by_record, + bsa_find_file_record, + bsa_add_file_from_memory, + bsa_add_file_from_disk_root, + bsa_add_file_from_disk, + bsa_save, + bsa_create_archive, + bsa_load_from_file, + bsa_create, + bsa_free + ; +end. diff --git a/libs/libbsarch/delphi/libbsarch.dproj b/libs/libbsarch/delphi/libbsarch.dproj new file mode 100644 index 0000000..1e928c6 --- /dev/null +++ b/libs/libbsarch/delphi/libbsarch.dproj @@ -0,0 +1,916 @@ + + + {381E4D59-4D34-46B3-B039-1B3C3ABEC58F} + libbsarch.dpr + True + Release + 3 + Library + None + 19.5 + Win64 + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + false + false + false + false + false + 00400000 + true + libbsarch + 1033 + CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=;CFBundleName= + System;Xml;Data;Datasnap;Web;Soap;Winapi;$(DCC_Namespace) + C:\dev\TES5Edit + $(XEditPath)\Core;$(XEditPath)\External\lz4;$(XEditPath)\External\TForge;$(XEditPath)\External\TForge\Source\Include;$(XEditPath)\External\TForge\Source\Shared;$(XEditPath)\External\TForge\Source\Engine\Hashes;$(XEditPath)\External\TForge\Source\Engine\Forge;$(XEditPath)\External\ZLibEx;$(XEditPath)\External\lz4\lib\delphi;$(XEditPath)\External\lz4\common;$(DCC_UnitSearchPath) + + + System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Debug + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) + 1033 + + + System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + Debug + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + (None) + none + + + RELEASE;$(DCC_Define) + 0 + false + 0 + + + Win32\Release\ + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) + (None) + Win32\Release\libbsarch-visualstudio-test.exe + + + X64\Release\ + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + (None) + X64\Release\libbsarch-visualstudio-test.exe + + + DEBUG;$(DCC_Define) + false + true + + + Win32\Debug\libbsarch-visualstudio-test.exe + Win32\Debug\ + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) + (None) + + + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + (None) + X64\Debug\ + D:\Projects\libbsarch\x64\Debug\libbsarch-visualstudio-test.exe + D:\Projects\libbsarch\x64\Debug + + + + MainSource + + + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + + + + Delphi.Personality.12 + + + + + libbsarch.dpr + + + Microsoft Office 2000 Sample Automation Server Wrapper Components + Microsoft Office XP Sample Automation Server Wrapper Components + + + + True + True + + False + + + + true + + + + + true + + + + + true + + + + + libbsarch.dll + true + + + + + 1 + + + 0 + + + + + classes + 64 + + + classes + 64 + + + + + res\xml + 1 + + + res\xml + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\armeabi + 1 + + + library\lib\armeabi + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\mips + 1 + + + library\lib\mips + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-v21 + 1 + + + res\values-v21 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-ldpi + 1 + + + res\drawable-ldpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-small + 1 + + + res\drawable-small + 1 + + + + + res\drawable-normal + 1 + + + res\drawable-normal + 1 + + + + + res\drawable-large + 1 + + + res\drawable-large + 1 + + + + + res\drawable-xlarge + 1 + + + res\drawable-xlarge + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + 1 + + + 1 + + + 0 + + + + + 1 + .framework + + + 1 + .framework + + + 1 + .framework + + + 0 + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 0 + .dll;.bpl + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 0 + .bpl + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 1 + + + 1 + + + + + + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 0 + + + + + library\lib\armeabi-v7a + 1 + + + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + + + + 1 + + + 1 + + + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + + + + + + + + + + + + 12 + + + + + diff --git a/libs/libbsarch/delphi/wbBSArchive.pas b/libs/libbsarch/delphi/wbBSArchive.pas new file mode 100644 index 0000000..533ee1c --- /dev/null +++ b/libs/libbsarch/delphi/wbBSArchive.pas @@ -0,0 +1,2637 @@ +{****************************************************************************** + + This Source Code Form is subject to the terms of the Mozilla Public License, + v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain + one at https://mozilla.org/MPL/2.0/. + +*******************************************************************************} + +unit wbBSArchive; + +interface + +uses + System.SysUtils, + System.Classes, + Winapi.Windows, + System.Threading, + System.SyncObjs, + System.Generics.Defaults, + System.Generics.Collections, + wbStreams, + tfTypes, + tfMD5; + +const + csBSAVersion = '0.9d'; + +type + // per file compression options + TPackingCompression = (pcGlobal, pcCompress, pcUncompress); + + TBGSCompressionType = (ctZlib, ctLZ4Frame, ctLZ4Block); + + TMagic4 = array [0..3] of AnsiChar; + PMagic4 = ^TMagic4; + + TDXGI = ( + DXGI_FORMAT_UNKNOWN, + DXGI_FORMAT_R32G32B32A32_TYPELESS, + DXGI_FORMAT_R32G32B32A32_FLOAT, + DXGI_FORMAT_R32G32B32A32_UINT, + DXGI_FORMAT_R32G32B32A32_SINT, + DXGI_FORMAT_R32G32B32_TYPELESS, + DXGI_FORMAT_R32G32B32_FLOAT, + DXGI_FORMAT_R32G32B32_UINT, + DXGI_FORMAT_R32G32B32_SINT, + DXGI_FORMAT_R16G16B16A16_TYPELESS, + DXGI_FORMAT_R16G16B16A16_FLOAT, + DXGI_FORMAT_R16G16B16A16_UNORM, + DXGI_FORMAT_R16G16B16A16_UINT, + DXGI_FORMAT_R16G16B16A16_SNORM, + DXGI_FORMAT_R16G16B16A16_SINT, + DXGI_FORMAT_R32G32_TYPELESS, + DXGI_FORMAT_R32G32_FLOAT, + DXGI_FORMAT_R32G32_UINT, + DXGI_FORMAT_R32G32_SINT, + DXGI_FORMAT_R32G8X24_TYPELESS, + DXGI_FORMAT_D32_FLOAT_S8X24_UINT, + DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS, + DXGI_FORMAT_X32_TYPELESS_G8X24_UINT, + DXGI_FORMAT_R10G10B10A2_TYPELESS, + DXGI_FORMAT_R10G10B10A2_UNORM, + DXGI_FORMAT_R10G10B10A2_UINT, + DXGI_FORMAT_R11G11B10_FLOAT, + DXGI_FORMAT_R8G8B8A8_TYPELESS, + DXGI_FORMAT_R8G8B8A8_UNORM, + DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, + DXGI_FORMAT_R8G8B8A8_UINT, + DXGI_FORMAT_R8G8B8A8_SNORM, + DXGI_FORMAT_R8G8B8A8_SINT, + DXGI_FORMAT_R16G16_TYPELESS, + DXGI_FORMAT_R16G16_FLOAT, + DXGI_FORMAT_R16G16_UNORM, + DXGI_FORMAT_R16G16_UINT, + DXGI_FORMAT_R16G16_SNORM, + DXGI_FORMAT_R16G16_SINT, + DXGI_FORMAT_R32_TYPELESS, + DXGI_FORMAT_D32_FLOAT, + DXGI_FORMAT_R32_FLOAT, + DXGI_FORMAT_R32_UINT, + DXGI_FORMAT_R32_SINT, + DXGI_FORMAT_R24G8_TYPELESS, + DXGI_FORMAT_D24_UNORM_S8_UINT, + DXGI_FORMAT_R24_UNORM_X8_TYPELESS, + DXGI_FORMAT_X24_TYPELESS_G8_UINT, + DXGI_FORMAT_R8G8_TYPELESS, + DXGI_FORMAT_R8G8_UNORM, + DXGI_FORMAT_R8G8_UINT, + DXGI_FORMAT_R8G8_SNORM, + DXGI_FORMAT_R8G8_SINT, + DXGI_FORMAT_R16_TYPELESS, + DXGI_FORMAT_R16_FLOAT, + DXGI_FORMAT_D16_UNORM, + DXGI_FORMAT_R16_UNORM, + DXGI_FORMAT_R16_UINT, + DXGI_FORMAT_R16_SNORM, + DXGI_FORMAT_R16_SINT, + DXGI_FORMAT_R8_TYPELESS, + DXGI_FORMAT_R8_UNORM, + DXGI_FORMAT_R8_UINT, + DXGI_FORMAT_R8_SNORM, + DXGI_FORMAT_R8_SINT, + DXGI_FORMAT_A8_UNORM, + DXGI_FORMAT_R1_UNORM, + DXGI_FORMAT_R9G9B9E5_SHAREDEXP, + DXGI_FORMAT_R8G8_B8G8_UNORM, + DXGI_FORMAT_G8R8_G8B8_UNORM, + DXGI_FORMAT_BC1_TYPELESS, + DXGI_FORMAT_BC1_UNORM, + DXGI_FORMAT_BC1_UNORM_SRGB, + DXGI_FORMAT_BC2_TYPELESS, + DXGI_FORMAT_BC2_UNORM, + DXGI_FORMAT_BC2_UNORM_SRGB, + DXGI_FORMAT_BC3_TYPELESS, + DXGI_FORMAT_BC3_UNORM, + DXGI_FORMAT_BC3_UNORM_SRGB, + DXGI_FORMAT_BC4_TYPELESS, + DXGI_FORMAT_BC4_UNORM, + DXGI_FORMAT_BC4_SNORM, + DXGI_FORMAT_BC5_TYPELESS, + DXGI_FORMAT_BC5_UNORM, + DXGI_FORMAT_BC5_SNORM, + DXGI_FORMAT_B5G6R5_UNORM, + DXGI_FORMAT_B5G5R5A1_UNORM, + DXGI_FORMAT_B8G8R8A8_UNORM, + DXGI_FORMAT_B8G8R8X8_UNORM, + DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, + DXGI_FORMAT_B8G8R8A8_TYPELESS, + DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, + DXGI_FORMAT_B8G8R8X8_TYPELESS, + DXGI_FORMAT_B8G8R8X8_UNORM_SRGB, + DXGI_FORMAT_BC6H_TYPELESS, + DXGI_FORMAT_BC6H_UF16, + DXGI_FORMAT_BC6H_SF16, + DXGI_FORMAT_BC7_TYPELESS, + DXGI_FORMAT_BC7_UNORM, + DXGI_FORMAT_BC7_UNORM_SRGB, + DXGI_FORMAT_AYUV, + DXGI_FORMAT_Y410, + DXGI_FORMAT_Y416, + DXGI_FORMAT_NV12, + DXGI_FORMAT_P010, + DXGI_FORMAT_P016, + DXGI_FORMAT_420_OPAQUE, + DXGI_FORMAT_YUY2, + DXGI_FORMAT_Y210, + DXGI_FORMAT_Y216, + DXGI_FORMAT_NV11, + DXGI_FORMAT_AI44, + DXGI_FORMAT_IA44, + DXGI_FORMAT_P8, + DXGI_FORMAT_A8P8, + DXGI_FORMAT_B4G4R4A4_UNORM, + DXGI_FORMAT_P208, + DXGI_FORMAT_V208, + DXGI_FORMAT_V408 + ); + + TDDSHeader = packed record + Magic: TMagic4; + dwSize: Cardinal; + dwFlags: Cardinal; + dwHeight: Cardinal; + dwWidth: Cardinal; + dwPitchOrLinearSize: Cardinal; + dwDepth: Cardinal; + dwMipMapCount: Cardinal; + dwReserved1: array [0..10] of Cardinal; + ddspf: packed record + dwSize: Cardinal; + dwFlags: Cardinal; + dwFourCC: TMagic4; + dwRGBBitCount: Cardinal; + dwRBitMask: Cardinal; + dwGBitMask: Cardinal; + dwBBitMask: Cardinal; + dwABitMask: Cardinal; + end; + dwCaps: Cardinal; + dwCaps2: Cardinal; + dwCaps3: Cardinal; + dwCaps4: Cardinal; + dwReserved2: Cardinal; + end; + PDDSHeader = ^TDDSHeader; + + TDDSHeaderDX10 = packed record + dxgiFormat: Integer; + resourceDimension: Cardinal; + miscFlags: Cardinal; + arraySize: Cardinal; + miscFlags2: Cardinal; + end; + PDDSHeaderDX10 = ^TDDSHeaderDX10; + +const + DDSD_CAPS = $00000001; + DDSD_HEIGHT = $00000002; + DDSD_WIDTH = $00000004; + DDSD_PITCH = $00000008; + DDSD_PIXELFORMAT = $00001000; + DDSD_MIPMAPCOUNT = $00020000; + DDSD_LINEARSIZE = $00080000; + DDSD_DEPTH = $00800000; + DDSCAPS_COMPLEX = $00000008; + DDSCAPS_TEXTURE = $00001000; + DDSCAPS_MIPMAP = $00400000; + DDSCAPS2_CUBEMAP = $00000200; + DDSCAPS2_POSITIVEX = $00000400; + DDSCAPS2_NEGATIVEX = $00000800; + DDSCAPS2_POSITIVEY = $00001000; + DDSCAPS2_NEGATIVEY = $00002000; + DDSCAPS2_POSITIVEZ = $00004000; + DDSCAPS2_NEGATIVEZ = $00008000; + DDSCAPS2_VOLUME = $00200000; + DDPF_ALPHAPIXELS = $00000001; + DDPF_ALPHA = $00000002; + DDPF_FOURCC = $00000004; + DDPF_RGB = $00000040; + DDPF_YUV = $00000200; + DDPF_LUMINANCE = $00020000; + + // DX10 + DDS_DIMENSION_TEXTURE2D = $00000003; + DDS_RESOURCE_MISC_TEXTURECUBE = $00000004; + +type + TwbBSArchive = class; + + TwbResourceDict = TDictionary; + + TBSArchiveType = (baNone, baTES3, baTES4, baFO3, baSSE, baFO4, baFO4dds, baSF, baSFdds); + TBSArchiveState = (stReading, stWriting); + TBSArchiveStates = set of TBSArchiveState; + TBSFileIterationProc = function(aArchive: Pointer; const aFileName: string; + aFileRecord: Pointer; aFolderRecord: Pointer; aData: Pointer): Boolean; stdcall; + + TDDSInfo = record Width, Height, MipMaps: Integer; end; + TBSFileDDSInfoProc = procedure(aArchive: Pointer; const aFileName: string; + var aInfo: TDDSInfo; aContext: Pointer); stdcall; + + TwbBSHeaderTES3 = packed record + HashOffset: Cardinal; + FileCount: Cardinal; + end; + + TwbBSFileTES3 = record + Hash: UInt64; + Size: Cardinal; + Offset: Cardinal; + Name: string; + end; + PwbBSFileTES3 = ^TwbBSFileTES3; + + TwbBSHeaderTES4 = packed record + FoldersOffset: Cardinal; + Flags: Cardinal; + FolderCount: Cardinal; + FileCount: Cardinal; + FolderNamesLength: Cardinal; + FileNamesLength: Cardinal; + FileFlags: Cardinal; + end; + + TwbBSFileTES4 = record + Hash: UInt64; + Size: Cardinal; + Offset: Int64; + Name: string; + PackingCompression: TPackingCompression; + function Compress(bsa: TwbBSArchive): Boolean; // compress when packing into a new archive + function Compressed(bsa: TwbBSArchive): Boolean; // compressed in existing archive + function RawSize: Cardinal; + end; + PwbBSFileTES4 = ^TwbBSFileTES4; + + TwbBSFolderTES4 = record + Hash: UInt64; + FileCount: Cardinal; + Unk32: Cardinal; + Offset: Int64; + Name: string; + Files: array of TwbBSFileTES4; + end; + PwbBSFolderTES4 = ^TwbBSFolderTES4; + + TwbBSHeaderFO4 = packed record + Magic: TMagic4; + FileCount: Cardinal; + FileTableOffset: Int64; + end; + + TwbBSHeaderSFv2 = packed record + Unknown1: Cardinal; + Unknown2: Cardinal; + end; + + TwbBSHeaderSFv3 = packed record + Unknown1: Cardinal; + Unknown2: Cardinal; + CompressionMethod: Cardinal; + end; + + TwbBSTexChunkRec = record + Size : Cardinal; + PackedSize : Cardinal; + Offset : Int64; + StartMip : Word; + EndMip : Word; + end; + PwbBSTexChunkRec = ^TwbBSTexChunkRec; + + TwbBSFileFO4 = record + NameHash: Cardinal; + Ext: TMagic4; + DirHash: Cardinal; + // GNRL archive format + Unknown: Cardinal; + Offset: Int64; + PackedSize: Cardinal; + Size: Cardinal; + // + // DX10 archive format + UnknownTex : Byte; + //ChunkHeaderSize: Word; + Height : Word; + Width : Word; + NumMips : Byte; + DXGIFormat : Byte; + CubeMaps : Word; + TexChunks : array of TwbBSTexChunkRec; + // + Name: string; + PackingCompression: TPackingCompression; + function DXGIFormatName: string; + function Compress(bsa: TwbBSArchive): Boolean; // compress when packing into a new archive + function Compressed(bsa: TwbBSArchive): Boolean; // compressed in existing archive + end; + PwbBSFileFO4 = ^TwbBSFileFO4; + + TPackedDataHash = TMD5Digest; + TPackedDataInfo = record + Size: Cardinal; + Hash: TPackedDataHash; + FileRecord: Pointer; + end; + PPackedDataInfo = ^TPackedDataInfo; + + TwbBSResultBuffer = packed record + size: Cardinal; + data: PByte; + end; + + TwbBSArchive = class + private + fStream: TwbBaseCachedFileStream; + fStates: TBSArchiveStates; + fType: TBSArchiveType; + fFileName: string; + fMagic: TMagic4; + fVersion: Cardinal; + fCompress: Boolean; + fCompressionType: TBGSCompressionType; + fShareData: Boolean; + fMultiThreaded: Boolean; + fDDSInfoProc: TBSFileDDSInfoProc; + fDDSInfoProcContext: Pointer; + + fHeaderTES3: TwbBSHeaderTES3; + fFilesTES3: array of TwbBSFileTES3; + + fHeaderTES4: TwbBSHeaderTES4; + fFoldersTES4: array of TwbBSFolderTES4; + + fHeaderFO4: TwbBSHeaderFO4; + fHeaderSFv2: TwbBSHeaderSFv2; + fHeaderSFv3: TwbBSHeaderSFv3; + fFilesFO4: array of TwbBSFileFO4; + fMaxChunkCount: Integer; + fSingleMipChunkX: Integer; + fSingleMipChunkY: Integer; + + fDataOffset: Int64; + + fPackedData: array of TPackedDataInfo; + fPackedDataCount: Integer; + + {$IF CompilerVersion >= 34.0} { Delphi 10.4 } + Sync: TLightweightMREW; + {$ELSE} + Sync: IReadWriteSync; + {$IFEND} + + function GetArchiveFormatName: string; + function GetFileCount: Cardinal; + function GetCreatedArchiveSize: Int64; + procedure SetArchiveFlags(aFlags: Cardinal); + procedure SetMultiThreaded(aValue: Boolean); + function FindFileRecordTES3(const aFileName: string; var aFileIdx: Integer): Boolean; + function FindFileRecordTES4(const aFileName: string; var aFolderIdx, aFileIdx: Integer): Boolean; + function FindFileRecordFO4(const aFileName: string; var aFileIdx: Integer): Boolean; + function GetDDSMipChunkNum(var aDDSInfo: TDDSInfo): Integer; + function CalcDataHash(aData: Pointer; aLen: Cardinal): TPackedDataHash; + function FindPackedData(aSize: Cardinal; aHash: TPackedDataHash; aFileRecord: Pointer): Boolean; + procedure AddPackedData(aSize: Cardinal; aHash: TPackedDataHash; aFileRecord: Pointer); + procedure PackData(aFileRecord: Pointer; const aFileName: string; + aDataHash: TPackedDataHash; aData: PByte; aSize: Integer; + aCompress: Boolean; aDoCompress: Boolean = False); + procedure CompressStream(aSrc, aDst: TStream); + procedure DecompressBuf(aSrc: Pointer; aSrcSize: Integer; aDst: Pointer; aDstSize: Integer); + + public + constructor Create; + destructor Destroy; override; + procedure LoadFromFile(const aFileName: string); + procedure CreateArchive(const aFileName: string; aType: TBSArchiveType; + aFilesList: TStringList = nil); + procedure Save; + procedure AddFileDisk(const aFilePath, aSourcePath: string); + procedure AddFileDiskRoot(const aRootDir, aSourcePath: string); + procedure AddFileData(const aFileName: string; const aSize: Cardinal; const aData: PByte); overload; + function FindFileRecord(const aFileName: string): Pointer; + function ExtractFileData(aFileRecord: Pointer): TwbBSResultBuffer; overload; + function ExtractFileData(const aFileName: string): TwbBSResultBuffer; overload; + procedure ReleaseFileData(fileDataResult: TwbBSResultBuffer); + procedure ExtractFile(const aFileName, aSaveAs: string); + procedure IterateFiles(aProc: TBSFileIterationProc; aData: Pointer = nil; + aSingleThreaded: Boolean = False); + function FileExists(const aFileName: string): Boolean; + procedure ResourceList(const aList: TStrings; aFolder: string = ''); + procedure ResourceDict(const aDict: TwbResourceDict; aFolder: string = ''); + //procedure IterateFolders(aProc: TBSFileIterationProc); + procedure Close; + + procedure SyncBeginWrite; + procedure SyncEndWrite; + + property FileName: string read fFileName; + property ArchiveType: TBSArchiveType read fType; + property Version: Cardinal read fVersion; + property FormatName: string read GetArchiveFormatName; + property FileCount: Cardinal read GetFileCount; + property CreatedArchiveSize: Int64 read GetCreatedArchiveSize; + property ArchiveFlags: Cardinal read fHeaderTES4.Flags write SetArchiveFlags; + property FileFlags: Cardinal read fHeaderTES4.FileFlags write fHeaderTES4.FileFlags; + property Compress: Boolean read fCompress write fCompress; + property ShareData: Boolean read fShareData write fShareData; + property MultiThreaded: Boolean read fMultiThreaded write SetMultiThreaded; + property DDSInfoProc: TBSFileDDSInfoProc read fDDSInfoProc write fDDSInfoProc; + property DDSInfoProcContext: Pointer read fDDSInfoProcContext write fDDSInfoProcContext; + end; + +const + cArchiveFormatNames: array[TBSArchiveType] of string = ( + 'None', + 'Morrowind', + 'Oblivion', + 'Skyrim LE, New Vegas, Fallout 3', + 'Skyrim SE, Skyrim AE', + 'Fallout 4', + 'Fallout 4 DDS', + 'Starfield', + 'Starfield DDS' + ); + + cArchiveTypeExtensions: array[TBSArchiveType] of string = ( + '.bsa', + '.bsa', + '.bsa', + '.bsa', + '.bsa', + '.ba2', + '.ba2', + '.ba2', + '.ba2' + ); + + cArchiveFlagNames: array [0..31] of string = ( + 'Include Directory Names', 'Include File Names', 'Compressed', + 'Retain Directory Names', 'Retain File Names', + 'Retain File Name Offsets', 'XBox 360 Archive', + 'Retain Strings During Startup', + 'Embed File Names', 'XMem Codec', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' + ); + + cFileFlagNames: array [0..31] of string = ( + 'Meshes', 'Textures', 'Menus', 'Sounds', + 'Voices', 'Shaders', 'Trees', 'Fonts', + 'Misc', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' + ); + +function SplitDirName(const aFileName: string; var Dir, Name: string): Integer; +function SplitNameExt(const aFileName: string; var Name, Ext: string; aNoExtDot: Boolean = False): Integer; +function CreateHashTES3(const aFileName: string): UInt64; +function CreateHashTES4(const aFileName: string): UInt64; overload; +function CreateHashTES4(const aName, aExt: string): UInt64; overload; +function CreateHashFO4(const aFileName: string): Cardinal; + +implementation + +uses + TypInfo, + zlibEx, + lz4io; + +const + MAGIC_TES3: TMagic4 = #0#1#0#0; + MAGIC_BSA : TMagic4 = 'BSA'#0; + MAGIC_BTDX: TMagic4 = 'BTDX'; + MAGIC_GNRL: TMagic4 = 'GNRL'; + MAGIC_DX10: TMagic4 = 'DX10'; + MAGIC_DDS : TMagic4 = 'DDS '; + MAGIC_DXT1: TMagic4 = 'DXT1'; + MAGIC_DXT3: TMagic4 = 'DXT3'; + MAGIC_DXT5: TMagic4 = 'DXT5'; + MAGIC_ATI1: TMagic4 = 'ATI1'; + MAGIC_ATI2: TMagic4 = 'ATI2'; + MAGIC_BC4S: TMagic4 = 'BC4S'; + MAGIC_BC4U: TMagic4 = 'BC4U'; + MAGIC_BC5S: TMagic4 = 'BC5S'; + MAGIC_BC5U: TMagic4 = 'BC5U'; + + iFileFO4Unknown = $00100100; + iFileFO4Tail = $BAADF00D; + + { https://github.com/jonwd7/bae/blob/master/src/bsa.h } + + // header versions + HEADER_VERSION_TES4 = $67; // Oblivion + HEADER_VERSION_FO3 = $68; // FO3, FNV, TES5 + HEADER_VERSION_SSE = $69; // SSE + HEADER_VERSION_FO4v1 = $01; // FO4 + HEADER_VERSION_SF2 = $02; // SF + HEADER_VERSION_SF3 = $03; // SF + HEADER_VERSION_FO4NGv7 = $07; // FO4NG + HEADER_VERSION_FO4NGv8 = $08; // FO4NG2 + + + // archive flags + ARCHIVE_PATHNAMES = $0001; // Whether the BSA has names for paths + ARCHIVE_FILENAMES = $0002; // Whether the BSA has names for files + ARCHIVE_COMPRESS = $0004; // Whether the files are compressed in archive (invert file's compression flag) + ARCHIVE_RETAINDIR = $0008; + ARCHIVE_RETAINNAME = $0010; + ARCHIVE_RETAINFOFF = $0020; + ARCHIVE_XBOX360 = $0040; + ARCHIVE_STARTUPSTR = $0080; + ARCHIVE_EMBEDNAME = $0100; // Whether the name is prefixed to the data + ARCHIVE_XMEM = $0200; + ARCHIVE_UNKNOWN10 = $0400; + + // file flags + FILE_NIF = $0001; + FILE_DDS = $0002; + FILE_XML = $0004; + FILE_WAV = $0008; + FILE_MP3 = $0010; + FILE_TXT = $0020; // TXT, HTML, BAT, SCC + FILE_SPT = $0040; + FILE_FNT = $0080; // TEX, FNT + FILE_MISC = $0100; // CTL and others + + FILE_SIZE_COMPRESS = $40000000; // Whether the file is compressed + + crc32table : array [0..255] of Cardinal = ( + $00000000, $77073096, $ee0e612c, $990951ba, $076dc419, $706af48f, + $e963a535, $9e6495a3, $0edb8832, $79dcb8a4, $e0d5e91e, $97d2d988, + $09b64c2b, $7eb17cbd, $e7b82d07, $90bf1d91, $1db71064, $6ab020f2, + $f3b97148, $84be41de, $1adad47d, $6ddde4eb, $f4d4b551, $83d385c7, + $136c9856, $646ba8c0, $fd62f97a, $8a65c9ec, $14015c4f, $63066cd9, + $fa0f3d63, $8d080df5, $3b6e20c8, $4c69105e, $d56041e4, $a2677172, + $3c03e4d1, $4b04d447, $d20d85fd, $a50ab56b, $35b5a8fa, $42b2986c, + $dbbbc9d6, $acbcf940, $32d86ce3, $45df5c75, $dcd60dcf, $abd13d59, + $26d930ac, $51de003a, $c8d75180, $bfd06116, $21b4f4b5, $56b3c423, + $cfba9599, $b8bda50f, $2802b89e, $5f058808, $c60cd9b2, $b10be924, + $2f6f7c87, $58684c11, $c1611dab, $b6662d3d, $76dc4190, $01db7106, + $98d220bc, $efd5102a, $71b18589, $06b6b51f, $9fbfe4a5, $e8b8d433, + $7807c9a2, $0f00f934, $9609a88e, $e10e9818, $7f6a0dbb, $086d3d2d, + $91646c97, $e6635c01, $6b6b51f4, $1c6c6162, $856530d8, $f262004e, + $6c0695ed, $1b01a57b, $8208f4c1, $f50fc457, $65b0d9c6, $12b7e950, + $8bbeb8ea, $fcb9887c, $62dd1ddf, $15da2d49, $8cd37cf3, $fbd44c65, + $4db26158, $3ab551ce, $a3bc0074, $d4bb30e2, $4adfa541, $3dd895d7, + $a4d1c46d, $d3d6f4fb, $4369e96a, $346ed9fc, $ad678846, $da60b8d0, + $44042d73, $33031de5, $aa0a4c5f, $dd0d7cc9, $5005713c, $270241aa, + $be0b1010, $c90c2086, $5768b525, $206f85b3, $b966d409, $ce61e49f, + $5edef90e, $29d9c998, $b0d09822, $c7d7a8b4, $59b33d17, $2eb40d81, + $b7bd5c3b, $c0ba6cad, $edb88320, $9abfb3b6, $03b6e20c, $74b1d29a, + $ead54739, $9dd277af, $04db2615, $73dc1683, $e3630b12, $94643b84, + $0d6d6a3e, $7a6a5aa8, $e40ecf0b, $9309ff9d, $0a00ae27, $7d079eb1, + $f00f9344, $8708a3d2, $1e01f268, $6906c2fe, $f762575d, $806567cb, + $196c3671, $6e6b06e7, $fed41b76, $89d32be0, $10da7a5a, $67dd4acc, + $f9b9df6f, $8ebeeff9, $17b7be43, $60b08ed5, $d6d6a3e8, $a1d1937e, + $38d8c2c4, $4fdff252, $d1bb67f1, $a6bc5767, $3fb506dd, $48b2364b, + $d80d2bda, $af0a1b4c, $36034af6, $41047a60, $df60efc3, $a867df55, + $316e8eef, $4669be79, $cb61b38c, $bc66831a, $256fd2a0, $5268e236, + $cc0c7795, $bb0b4703, $220216b9, $5505262f, $c5ba3bbe, $b2bd0b28, + $2bb45a92, $5cb36a04, $c2d7ffa7, $b5d0cf31, $2cd99e8b, $5bdeae1d, + $9b64c2b0, $ec63f226, $756aa39c, $026d930a, $9c0906a9, $eb0e363f, + $72076785, $05005713, $95bf4a82, $e2b87a14, $7bb12bae, $0cb61b38, + $92d28e9b, $e5d5be0d, $7cdcefb7, $0bdbdf21, $86d3d2d4, $f1d4e242, + $68ddb3f8, $1fda836e, $81be16cd, $f6b9265b, $6fb077e1, $18b74777, + $88085ae6, $ff0f6a70, $66063bca, $11010b5c, $8f659eff, $f862ae69, + $616bffd3, $166ccf45, $a00ae278, $d70dd2ee, $4e048354, $3903b3c2, + $a7672661, $d06016f7, $4969474d, $3e6e77db, $aed16a4a, $d9d65adc, + $40df0b66, $37d83bf0, $a9bcae53, $debb9ec5, $47b2cf7f, $30b5ffe9, + $bdbdf21c, $cabac28a, $53b39330, $24b4a3a6, $bad03605, $cdd70693, + $54de5729, $23d967bf, $b3667a2e, $c4614ab8, $5d681b02, $2a6f2b94, + $b40bbe37, $c30c8ea1, $5a05df1b, $2d02ef8d + ); + +type + TPreallocatedMemoryStream = class(TCustomMemoryStream) + public + constructor Create(Ptr: Pointer; Size: Int64); + function Write(const Buffer; Count: Longint): Longint; override; + end; + +constructor TPreallocatedMemoryStream.Create(Ptr: Pointer; Size: Int64); +begin + inherited Create; + SetPointer(Ptr, Size); +end; + +function TPreallocatedMemoryStream.Write(const Buffer; Count: Integer): Longint; +begin + Result := Size-Position; + if Result > Count then + Result := Count; + System.Move(Buffer, Pointer(PByte(Memory) + Position)^, Result); + Seek(Result, soCurrent); +end; + +function Magic2Int(aMagic: TMagic4): Cardinal; inline; +begin + Result := PCardinal(@aMagic)^; +end; + +function Int2Magic(aInt: Cardinal): TMagic4; inline; +begin + Result := PMagic4(@aInt)^; +end; + +function String2Magic(const aStr: string): TMagic4; +begin + Result := #0#0#0#0; + if Length(aStr) > 0 then Result[0] := AnsiChar(aStr[1]); + if Length(aStr) > 1 then Result[1] := AnsiChar(aStr[2]); + if Length(aStr) > 2 then Result[2] := AnsiChar(aStr[3]); + if Length(aStr) > 3 then Result[3] := AnsiChar(aStr[4]); +end; + +function LowerByte(ch: AnsiChar): Byte; inline; +begin + case ch of + 'A'..'Z': + Result := Byte(Ord(ch) + Ord('a')-Ord('A')); + else + Result := Byte(ch); + end; +end; + +function LastCharPos(const s: string; const Chr: char): Integer; inline; +begin + for Result := Length(s) downto 1 do + if s[Result] = Chr then + Exit; + Result := 0; +end; + +function Str2MagicInt(const s: string): Cardinal; +var + i: integer; +begin + Result := 0; + for i := 1 to Length(s) do begin + PByte(PByte(@Result) + i-1)^ := LowerByte(AnsiChar(s[i])); + if i = 4 then Break; + end; +end; + +function SplitDirName(const aFileName: string; var Dir, Name: string): Integer; +begin + Result := LastCharPos(aFileName, '\'); + if Result = 0 then + Result := LastCharPos(aFileName, '/'); + if Result <> 0 then begin + Dir := Copy(aFileName, 1, Pred(Result)); + Name := Copy(aFileName, Succ(Result), Length(aFileName) - Result); + end + else begin + Dir := ''; + Name := aFileName; + end; +end; + +function SplitNameExt(const aFileName: string; var Name, Ext: string; aNoExtDot: Boolean = False): Integer; +begin + Result := LastCharPos(aFileName, '.'); + if Result <> 0 then begin + Name := Copy(aFileName, 1, Pred(Result)); + if aNoExtDot then + Inc(Result); + Ext := Copy(aFileName, Result, Length(aFileName) - Result + 1); + end + else begin + Name := aFileName; + Ext := ''; + end; +end; + +function CreateHashTES3(const aFileName: string): UInt64; +var + s: AnsiString; + i, l: integer; + sum, off, temp, n: Cardinal; +begin + s := AnsiString(aFileName); + l := Length(s) shr 1; + + sum := 0; off := 0; + for i := 1 to l do begin + temp := Cardinal(LowerByte(s[i])) shl (off and $1F); + sum := sum xor temp; + off := off + 8; + end; + Result := UInt64(sum) shl 32; + + sum := 0; off := 0; + for i := l + 1 to Length(s) do begin + temp := Cardinal(LowerByte(s[i])) shl (off and $1F); + sum := sum xor temp; + n := temp and $1F; + sum := (sum shr n) or (sum shl (32 - n)); + off := off + 8; + end; + Result := Result or sum; +end; + +function CreateHashTES4(const aName, aExt: string): UInt64; overload; +var + i, l: integer; + hash: Cardinal; + ext: array [0..3] of Byte; + s, e: AnsiString; +begin + Result := 0; + s := AnsiString(aName); + e := AnsiString(aExt); + + l := Length(s); + if l = 0 then + Exit; + + Result := LowerByte(s[l]); + if l > 2 then + Result := Result or (Cardinal(LowerByte(s[l-1])) shl 8); + Result := Result or (l shl 16); + Result := Result or (Cardinal(LowerByte(s[1])) shl 24); + + PCardinal(@ext)^ := 0; + for i := 1 to Length(e) do begin + ext[i-1] := LowerByte(e[i]); + if i = 4 then Break; + end; + + case PCardinal(@ext)^ of + $00666B2E: Result := Result or $80; // .kf + $66696E2E: Result := Result or $8000; // .nif + $7364642E: Result := Result or $8080; // .dds + $7661772E: Result := Result or $80000000; // .wav + end; + + hash := 0; + for i := 2 to l-2 do + hash := LowerByte(s[i]) + (hash shl 6) + (hash shl 16) - hash; + Result := Result + UInt64(hash) shl 32; + + hash := 0; + for i := 1 to Length(e) do + hash := LowerByte(e[i]) + (hash shl 6) + (hash shl 16) - hash; + Result := Result + UInt64(hash) shl 32; +end; + +function CreateHashTES4(const aFileName: string): UInt64; overload; +var + fname, fext: string; +begin + SplitNameExt(aFileName, fname, fext); + Result := CreateHashTES4(fname, fext); +end; + +function CreateHashFO4(const aFileName: string): Cardinal; +var + i: Integer; + s: AnsiString; + c: AnsiChar; +begin + Result := 0; + s := AnsiString(aFileName); + for i := 1 to Length(s) do begin + c := s[i]; + if Byte(c) > 127 then Continue; + if c = '/' then c := '\'; + Result := (Result shr 8) xor crc32table[(Result xor LowerByte(c)) and $FF]; + end; +end; + +function TwbBSFileTES4.Compress(bsa: TwbBSArchive): Boolean; +begin + case PackingCompression of + pcCompress : Result := True; + pcUncompress: Result := False; + else + Result := bsa.Compress; + end; +end; + +function TwbBSFileTES4.Compressed(bsa: TwbBSArchive): Boolean; +begin + Result := (bsa.ArchiveFlags and ARCHIVE_COMPRESS <> 0) xor (Size and FILE_SIZE_COMPRESS <> 0); +end; + +function TwbBSFileTES4.RawSize: Cardinal; +begin + Result := Size and not FILE_SIZE_COMPRESS; +end; + +function TwbBSFileFO4.DXGIFormatName: string; +begin + Result := GetEnumName(TypeInfo(TDXGI), Integer(DXGIFormat)); +end; + +function TwbBSFileFO4.Compress(bsa: TwbBSArchive): Boolean; +begin + case PackingCompression of + pcCompress : Result := True; + pcUncompress: Result := False; + else + Result := bsa.Compress; + end; +end; + +function TwbBSFileFO4.Compressed(bsa: TwbBSArchive): Boolean; +begin + if bsa.ArchiveType = baFO4 then + Result := PackedSize <> 0 + else + Result := (Length(TexChunks) <> 0) and (TexChunks[0].PackedSize <> 0); +end; + + +{ TwbBSArchive } + +constructor TwbBSArchive.Create; +begin + fType := baNone; + fMaxChunkCount := 4; + fSingleMipChunkX := 512; + fSingleMipChunkY := 512; +end; + +destructor TwbBSArchive.Destroy; +begin + if fStates * [stReading, stWriting] <> [] then + Close; +end; + +function TwbBSArchive.GetArchiveFormatName: string; +begin + Result := cArchiveFormatNames[fType]; +end; + +function TwbBSArchive.GetFileCount: Cardinal; +begin + case fType of + baTES3: + Result := fHeaderTES3.FileCount; + baTES4, baFO3, baSSE: + Result := fHeaderTES4.FileCount; + baFO4, baFO4dds, baSF, baSFdds: + Result := fHeaderFO4.FileCount; + else + Result := 0; + end; +end; + +procedure TwbBSArchive.SetArchiveFlags(aFlags: Cardinal); +begin + if not (fType in [baTES4, baFO3, baSSE]) then + raise Exception.Create('Archive flags are not supported for this archive type'); + + fHeaderTES4.Flags := aFlags; + + // force compression flag if needed + if fCompress then + fHeaderTES4.Flags := fHeaderTES4.Flags or ARCHIVE_COMPRESS; +end; + +procedure TwbBSArchive.SetMultiThreaded(aValue: Boolean); +begin + fMultiThreaded := aValue; + {$IF CompilerVersion < 34.0} + if aValue and not Assigned(Sync) then + Sync := TReadWriteSync.Create; + {$IFEND} +end; + +function TwbBSArchive.FindFileRecordTES3(const aFileName: string; var aFileIdx: Integer): Boolean; +var + h: UInt64; + i: integer; +begin + h := CreateHashTES3(aFileName); + + Result := False; + for i := Low(fFilesTES3) to High(fFilesTES3) do + if fFilesTES3[i].Hash = h then begin + aFileIdx := i; + Result := True; + Exit; + end; +end; + +function TwbBSArchive.FindFileRecordTES4(const aFileName: string; var aFolderIdx, aFileIdx: Integer): Boolean; +var + fdir, fname, name, ext: string; + h: UInt64; + i, j: integer; +begin + SplitDirName(aFileName, fdir, fname); + Result := False; + h := CreateHashTES4(fdir, ''); + for i := Low(fFoldersTES4) to High(fFoldersTES4) do begin + if h <> fFoldersTES4[i].Hash then + // since table is sorted by hash, we can abort when our hash is lesser + if h < fFoldersTES4[i].Hash then + Exit + else + Continue; + + SplitNameExt(fname, name, ext); + h := CreateHashTES4(name, ext); + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do begin + if h <> fFoldersTES4[i].Files[j].Hash then + if h < fFoldersTES4[i].Files[j].Hash then + Exit + else + Continue; + + Result := True; + aFolderIdx := i; + aFileIdx := j; + Exit; + end; + end; +end; + +function TwbBSArchive.FindFileRecordFO4(const aFileName: string; var aFileIdx: Integer): Boolean; +var + fdir, fname, name, ext: string; + hdir, hfile: Cardinal; + hext: TMagic4; + i: integer; +begin + SplitDirName(aFileName, fdir, fname); + SplitNameExt(fname, name, ext, True); + hdir := CreateHashFO4(fdir); + hfile := CreateHashFO4(name); + hext := String2Magic(LowerCase(ext)); + + Result := False; + for i := Low(fFilesFO4) to High(fFilesFO4) do + if (fFilesFO4[i].DirHash = hdir) and (fFilesFO4[i].NameHash = hfile) and (fFilesFO4[i].Ext = hext) then begin + aFileIdx := i; + Result := True; + Exit; + end; +end; + +function TwbBSArchive.FindFileRecord(const aFileName: string): Pointer; +var + i, j: integer; +begin + Result := nil; + + case fType of + baTES3: + if FindFileRecordTES3(aFileName, i) then Result := @fFilesTES3[i]; + baTES4, baFO3, baSSE: + if FindFileRecordTES4(aFileName, i, j) then Result := @fFoldersTES4[i].Files[j]; + baFO4, baFO4dds, baSF, baSFdds: + if FindFileRecordFO4(aFileName, i) then Result := @fFilesFO4[i]; + end; +end; + +function TwbBSArchive.GetDDSMipChunkNum(var aDDSInfo: TDDSInfo): Integer; +var + w, h: Integer; +begin + w := aDDSInfo.Width; + h := aDDSInfo.Height; + Result := 1; + while (Result < aDDSInfo.MipMaps) and + (Result < fMaxChunkCount) and + (w >= fSingleMipChunkX) and + (h >= fSingleMipChunkY) + do begin + Inc(Result); + w := w div 2; + h := h div 2; + end; +end; + +function TwbBSArchive.CalcDataHash(aData: Pointer; aLen: Cardinal): TPackedDataHash; +var + fMD5: TMD5Alg; +begin + fMD5.Init(@fMD5); + fMD5.Update(@fMD5, aData, aLen); + fMD5.Done(@fMD5, @Result); +end; + +function TwbBSArchive.FindPackedData(aSize: Cardinal; aHash: TPackedDataHash; aFileRecord: Pointer): Boolean; +var + i: Integer; +begin + Result := False; + + if not fShareData then + Exit; + + for i := 0 to Pred(fPackedDataCount) do + if (aSize = fPackedData[i].Size) and CompareMem(@aHash, @fPackedData[i].Hash, SizeOf(aHash)) then begin + case fType of + baTES3: begin + PwbBSFileTES3(aFileRecord).Size := PwbBSFileTES3(fPackedData[i].FileRecord).Size; + PwbBSFileTES3(aFileRecord).Offset := PwbBSFileTES3(fPackedData[i].FileRecord).Offset; + end; + baTES4, baFO3, baSSE: begin + PwbBSFileTES4(aFileRecord).Size := PwbBSFileTES4(fPackedData[i].FileRecord).Size; + PwbBSFileTES4(aFileRecord).Offset := PwbBSFileTES4(fPackedData[i].FileRecord).Offset; + end; + baFO4, baSF: begin + PwbBSFileFO4(aFileRecord).Size := PwbBSFileFO4(fPackedData[i].FileRecord).Size; + PwbBSFileFO4(aFileRecord).PackedSize := PwbBSFileFO4(fPackedData[i].FileRecord).PackedSize; + PwbBSFileFO4(aFileRecord).Offset := PwbBSFileFO4(fPackedData[i].FileRecord).Offset; + end; + baFO4dds, baSFdds: begin + PwbBSTexChunkRec(aFileRecord).Size := PwbBSTexChunkRec(fPackedData[i].FileRecord).Size; + PwbBSTexChunkRec(aFileRecord).PackedSize := PwbBSTexChunkRec(fPackedData[i].FileRecord).PackedSize; + PwbBSTexChunkRec(aFileRecord).Offset := PwbBSTexChunkRec(fPackedData[i].FileRecord).Offset; + end; + end; + Result := True; + Exit; + end; +end; + +procedure TwbBSArchive.AddPackedData(aSize: Cardinal; aHash: TPackedDataHash; aFileRecord: Pointer); +begin + if not fShareData then + Exit; + + if fPackedDataCount = Length(fPackedData) then + if Length(fPackedData) = 0 then + SetLength(fPackedData, 2048) + else + SetLength(fPackedData, Length(fPackedData) * 2); + + fPackedData[fPackedDataCount].Size := aSize; + fPackedData[fPackedDataCount].Hash := aHash; + fPackedData[fPackedDataCount].FileRecord := aFileRecord; + Inc(fPackedDataCount); +end; + +procedure TwbBSArchive.LoadFromFile(const aFileName: string); +var + i, j: Integer; +begin + if fStates * [stReading, stWriting] <> [] then + Close; + + fStream := TwbReadOnlyCachedFileStream.Create(aFileName, fmOpenRead or fmShareDenyWrite); + + // magic + fMagic := Int2Magic(fStream.ReadCardinal); + if fMagic = MAGIC_TES3 then fType := baTES3 else + if fMagic = MAGIC_BSA then fType := baTES4 else + if fMagic = MAGIC_BTDX then fType := baFO4 else + raise Exception.Create('Unknown archive format'); + + // archive version except Morrowind + if fType <> baTES3 then begin + fVersion := fStream.ReadCardinal; + fCompressionType := ctZlib; // default compression type + case fVersion of + HEADER_VERSION_TES4: + fType := baTES4; + HEADER_VERSION_FO3 : + fType := baFO3; + HEADER_VERSION_SSE : + fType := baSSE; + HEADER_VERSION_FO4v1, + HEADER_VERSION_FO4NGv7, + HEADER_VERSION_FO4NGv8 : + fType := baFO4; + HEADER_VERSION_SF2, + HEADER_VERSION_SF3: + fType := baSF; + else + raise Exception.Create('Unknown archive version 0x' + IntToHex(fVersion, 8)); + end; + end; + + case fType of + //-------------------------------------------------- + // Morrowind + baTES3: begin + // read header + fStream.ReadBuffer(fHeaderTES3, SizeOf(fHeaderTES3)); + SetLength(fFilesTES3, fHeaderTES3.FileCount); + for i := Low(fFilesTES3) to High(fFilesTES3) do begin + fFilesTES3[i].Size := fStream.ReadCardinal; + fFilesTES3[i].Offset := fStream.ReadCardinal; + end; + // skip name offsets + fStream.Position := fStream.Position + 4 * fHeaderTES3.FileCount; + // read names + for i := Low(fFilesTES3) to High(fFilesTES3) do + fFilesTES3[i].Name := fStream.ReadStringTerm; + // read hashes + for i := Low(fFilesTES3) to High(fFilesTES3) do + fFilesTES3[i].Hash := fStream.ReadUInt64; + // remember binary data offset since stored files offsets are relative + fDataOffset := fStream.Position; + end; + + //-------------------------------------------------- + // Fallout 4, Starfield + baFO4, baSF: begin + // read header + fStream.ReadBuffer(fHeaderFO4, SizeOf(fHeaderFO4)); + // SF header + if fType = baSF then + if fVersion = HEADER_VERSION_SF2 then + fStream.ReadBuffer(fHeaderSFv2, SizeOf(fHeaderSFv2)) + else begin + fStream.ReadBuffer(fHeaderSFv3, SizeOf(fHeaderSFv3)); + if fHeaderSFv3.CompressionMethod = 3 then + fCompressionType := ctLZ4Block; + end; + + // read GNRL files + if fHeaderFO4.Magic = MAGIC_GNRL then begin + SetLength(fFilesFO4, fHeaderFO4.FileCount); + for i := Low(fFilesFO4) to High(fFilesFO4) do begin + fFilesFO4[i].NameHash := fStream.ReadCardinal; + fFilesFO4[i].Ext := Int2Magic(fStream.ReadCardinal); + fFilesFO4[i].DirHash := fStream.ReadCardinal; + fFilesFO4[i].Unknown := fStream.ReadCardinal; + fFilesFO4[i].Offset := fStream.ReadInt64; + fFilesFO4[i].PackedSize := fStream.ReadCardinal; + fFilesFO4[i].Size := fStream.ReadCardinal; + fStream.ReadCardinal; // BAADF00D + end; + end + // read DX10 textures + else if fHeaderFO4.Magic = MAGIC_DX10 then begin + if fType = baFO4 then + fType := baFO4dds + else if fType = baSF then + fType := baSFdds; + SetLength(fFilesFO4, fHeaderFO4.FileCount); + for i := Low(fFilesFO4) to High(fFilesFO4) do begin + fFilesFO4[i].NameHash := fStream.ReadCardinal; + fFilesFO4[i].Ext := Int2Magic(fStream.ReadCardinal); + fFilesFO4[i].DirHash := fStream.ReadCardinal; + fFilesFO4[i].UnknownTex := fStream.ReadByte; + SetLength(fFilesFO4[i].TexChunks, fStream.ReadByte); + fStream.ReadWord; // skip chunkHeaderSize, always 24 + //fFilesFO4[i].ChunkHeaderSize := fStream.ReadWord; + fFilesFO4[i].Height := fStream.ReadWord; + fFilesFO4[i].Width := fStream.ReadWord; + fFilesFO4[i].NumMips := fStream.ReadByte; + fFilesFO4[i].DXGIFormat := fStream.ReadByte; + fFilesFO4[i].CubeMaps := fStream.ReadWord; + for j := Low(fFilesFO4[i].TexChunks) to High(fFilesFO4[i].TexChunks) do + with fFilesFO4[i].TexChunks[j] do begin + Offset := fStream.ReadInt64; + PackedSize := fStream.ReadCardinal; + Size := fStream.ReadCardinal; + StartMip := fStream.ReadWord; + EndMip := fStream.ReadWord; + fStream.ReadCardinal; // skip BAADF00D + end; + end; + end + else + raise Exception.Create('Unknown BA2 archive type'); + + // read file names + fStream.Position := fHeaderFO4.FileTableOffset; + for i := Low(fFilesFO4) to High(fFilesFO4) do + fFilesFO4[i].Name := StringReplace(fStream.ReadStringLen16, '/', '\', [rfReplaceAll]); + end; + + //-------------------------------------------------- + // Oblivion, Fallout 3, New Vegas, Skyrim, Skyrim SE + baTES4, baFO3, baSSE: begin + if fType = baSSE then + fCompressionType := ctLZ4Frame; + + // read header + fStream.ReadBuffer(fHeaderTES4, SizeOf(fHeaderTES4)); + fStream.Position := fHeaderTES4.FoldersOffset; + + // read folder records + SetLength(fFoldersTES4, fHeaderTES4.FolderCount); + for i := Low(fFoldersTES4) to High(fFoldersTES4) do begin + fFoldersTES4[i].Hash := fStream.ReadUInt64; + fFoldersTES4[i].FileCount := fStream.ReadCardinal; + if fType = baSSE then begin + fFoldersTES4[i].Unk32 := fStream.ReadCardinal; + fFoldersTES4[i].Offset := fStream.ReadInt64; + end else + fFoldersTES4[i].Offset := fStream.ReadCardinal; + end; + + // read folder names and file records + for i := Low(fFoldersTES4) to High(fFoldersTES4) do begin + fFoldersTES4[i].Name := fStream.ReadStringLen; + SetLength(fFoldersTES4[i].Files, fFoldersTES4[i].FileCount); + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do begin + fFoldersTES4[i].Files[j].Hash := fStream.ReadUInt64; + fFoldersTES4[i].Files[j].Size := fStream.ReadCardinal; + fFoldersTES4[i].Files[j].Offset := fStream.ReadCardinal; + end; + end; + + // read file names + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do + fFoldersTES4[i].Files[j].Name := fStream.ReadStringTerm; + end; + + end; + + fFileName := aFileName; + Include(fStates, stReading); +end; + + +type + THashPair = record DirHash, FileHash: UInt64; pc: TPackingCompression end; + PHashPair = ^THashPair; + +function HashPairSort(List: TStringList; Index1, Index2: Integer): Integer; +var + h1, h2: PHashPair; +begin + h1 := PHashPair(List.Objects[Index1]); + h2 := PHashPair(List.Objects[Index2]); + if h1.DirHash < h2.DirHash then + Result := -1 + else if h1.DirHash > h2.DirHash then + Result := 1 + else if h1.FileHash < h2.FileHash then + Result := -1 + else if h1.FileHash > h2.FileHash then + Result := 1 + else + Result := 0; +end; + +function AlphabeticalSort(List: TStringList; Index1, Index2: Integer): Integer; +begin + Result := CompareStr(List[Index1], List[Index2]); +end; + +procedure TwbBSArchive.CreateArchive(const aFileName: string; aType: TBSArchiveType; + aFilesList: TStringList = nil); +var + HashPairs: array of THashPair; + h: PHashPair; + hdir: UInt64; + s, fdir, fname, fext, name: string; + i, len, folderidx, fileidx: Integer; + Buffer: TBytes; + ddsinfo: TDDSInfo; +begin + if stReading in fStates then + Close; + + if stWriting in fStates then + raise Exception.Create('Archive is already being created'); + + case aType of + baTES3: begin + fMagic := MAGIC_TES3; + end; + baTES4: begin + fVersion := HEADER_VERSION_TES4; + fMagic := MAGIC_BSA; + fHeaderTES4.Flags := ARCHIVE_PATHNAMES or ARCHIVE_FILENAMES or ARCHIVE_EMBEDNAME or ARCHIVE_XMEM or ARCHIVE_UNKNOWN10; + fHeaderTES4.FileFlags := 0; + fHeaderTES4.FoldersOffset := SizeOf(fMagic) + SizeOf(fVersion) + SizeOf(fHeaderTES4); + fCompressionType := ctZlib; + end; + baFO3: begin + fVersion := HEADER_VERSION_FO3; + fMagic := MAGIC_BSA; + fHeaderTES4.Flags := ARCHIVE_PATHNAMES or ARCHIVE_FILENAMES; + fHeaderTES4.FileFlags := 0; + fHeaderTES4.FoldersOffset := SizeOf(fMagic) + SizeOf(fVersion) + SizeOf(fHeaderTES4); + fCompressionType := ctZlib; + end; + baSSE: begin + fVersion := HEADER_VERSION_SSE; + fMagic := MAGIC_BSA; + fHeaderTES4.Flags := ARCHIVE_PATHNAMES or ARCHIVE_FILENAMES; + fHeaderTES4.FileFlags := 0; + fHeaderTES4.FoldersOffset := SizeOf(fMagic) + SizeOf(fVersion) + SizeOf(fHeaderTES4); + fCompressionType := ctLZ4Frame; + end; + baFO4: begin + fMagic := MAGIC_BTDX; + fHeaderFO4.Magic := MAGIC_GNRL; + fVersion := HEADER_VERSION_FO4v1; + fCompressionType := ctZlib; + end; + baFO4dds: begin + fMagic := MAGIC_BTDX; + fHeaderFO4.Magic := MAGIC_DX10; + fVersion := HEADER_VERSION_FO4v1; + fCompressionType := ctZlib; + end; + baSF: begin + fMagic := MAGIC_BTDX; + fHeaderFO4.Magic := MAGIC_GNRL; + fVersion := HEADER_VERSION_SF2; + fCompressionType := ctZlib; + end; + baSFdds: begin + fMagic := MAGIC_BTDX; + fHeaderFO4.Magic := MAGIC_DX10; + fVersion := HEADER_VERSION_SF3; + fCompressionType := ctLZ4Block; + end; + else + raise Exception.Create('Unsupported archive type'); + end; + + fType := aType; + + if fType in [baTES3] then begin + if not Assigned(aFilesList) or (aFilesList.Count = 0) then + raise Exception.Create('Archive requires predefined files list'); + + // sort files by hashes + SetLength(HashPairs, aFilesList.Count); + for i := 0 to Pred(aFilesList.Count) do begin + h := @HashPairs[i]; + h.FileHash := CreateHashTES3(aFilesList[i]); + aFilesList.Objects[i] := Pointer(h); + end; + aFilesList.CustomSort(HashPairSort); + + // create file records and calculate total names length + SetLength(fFilesTES3, aFilesList.Count); + len := 0; + for i := 0 to Pred(aFilesList.Count) do begin + fFilesTES3[i].Hash := PHashPair(aFilesList.Objects[i]).FileHash; + fFilesTES3[i].Name := LowerCase(aFilesList[i]); + Inc(len, Length(fFilesTES3[i].Name) + 1); // include terminator + end; + + // offset to hash table + fDataOffset := SizeOf(fMagic) + SizeOf(fHeaderTES3) + + 8 * Length(fFilesTES3) + // File sizes/offsets + 4 * Length(fFilesTES3) + // Archive directory/name offsets + len; // Filename records + + // stored as minus 12 (for header size) + fHeaderTES3.HashOffset := fDataOffset - 12; + fHeaderTES3.FileCount := aFilesList.Count; + + // offset to files data + fDataOffset := fDataOffset + 8 * Length(fFilesTES3); // Hash table + + // files are stored alphabetically in the data section in vanilla archives + // not really needed but whatever + aFilesList.CustomSort(AlphabeticalSort); + end + + else if fType in [baTES4, baFO3, baSSE] then begin + if not Assigned(aFilesList) or (aFilesList.Count = 0) then + raise Exception.Create('Archive requires predefined files list'); + + fHeaderTES4.FolderNamesLength := 0; + fHeaderTES4.FileNamesLength := 0; + + // dirs and files must be sorted by their hashes + SetLength(HashPairs, aFilesList.Count); + for i := 0 to Pred(aFilesList.Count) do begin + h := @HashPairs[i]; + if SplitDirName(aFilesList[i], fdir, fname) = 0 then + raise Exception.Create('File is missing the folder part: ' + aFilesList[i]); + + // calculate hashes + h.DirHash := CreateHashTES4(fdir, ''); + SplitNameExt(fname, s, fext); + h.FileHash := CreateHashTES4(s, fext); + h.pc := TPackingCompression(aFilesList.Objects[i]); + aFilesList.Objects[i] := Pointer(h); + + fext := LowerCase(fext); + with fHeaderTES4 do begin + // determine file flags + if fdir.StartsWith('meshes\', True) then FileFlags := FileFlags or FILE_NIF else + if fdir.StartsWith('textures\', True) then FileFlags := FileFlags or FILE_DDS else + if fdir.StartsWith('sound\', True) then FileFlags := FileFlags or FILE_WAV or FILE_MP3 else + if fext = '.nif' then FileFlags := FileFlags or FILE_NIF else + if fext = '.lod' then FileFlags := FileFlags or FILE_NIF else + if fext = '.bto' then FileFlags := FileFlags or FILE_NIF else + if fext = '.btr' then FileFlags := FileFlags or FILE_NIF else + if fext = '.btt' then FileFlags := FileFlags or FILE_NIF else + if fext = '.dtl' then FileFlags := FileFlags or FILE_NIF else + if fext = '.kf' then FileFlags := FileFlags or FILE_NIF else + if fext = '.kfm' then FileFlags := FileFlags or FILE_NIF else + if fext = '.hkx' then FileFlags := FileFlags or FILE_NIF else + if fext = '.dds' then FileFlags := FileFlags or FILE_DDS else + if fext = '.xml' then FileFlags := FileFlags or FILE_XML or FILE_MISC else + if fext = '.wav' then FileFlags := FileFlags or FILE_WAV else + if fext = '.fuz' then FileFlags := FileFlags or FILE_WAV else + if fext = '.lip' then FileFlags := FileFlags or FILE_MP3 else + if fext = '.mp3' then FileFlags := FileFlags or FILE_MP3 else + if fext = '.ogg' then FileFlags := FileFlags or FILE_MP3 else + if fext = '.txt' then FileFlags := FileFlags or FILE_TXT else + if fext = '.htm' then FileFlags := FileFlags or FILE_TXT else + if fext = '.bat' then FileFlags := FileFlags or FILE_TXT else + if fext = '.scc' then FileFlags := FileFlags or FILE_TXT else + if fext = '.spt' then FileFlags := FileFlags or FILE_SPT else + if fext = '.fnt' then FileFlags := FileFlags or FILE_FNT else + if fext = '.tex' then FileFlags := FileFlags or FILE_FNT else + FileFlags := FileFlags or FILE_MISC; + + // determine archive flags + + // packed scripts can't be added to objects in the SSE CK if the archive was packed + // without the "RetainNames" flag (the scripts aren't shown in the script adding window) + if fext = '.pex' then ArchiveFlags := ArchiveFlags or ARCHIVE_RETAINNAME; + end; + end; + + // sort by hashes + aFilesList.CustomSort(HashPairSort); + + // create folder and file records + fHeaderTES4.FileCount := 0; + hdir := 0; + folderidx := -1; + fileidx := 0; + for i := 0 to Pred(aFilesList.Count) do begin + SplitDirName(aFilesList[i], fdir, fname); + h := Pointer(aFilesList.Objects[i]); + // new folder + if h.DirHash <> hdir then begin + Inc(folderidx); + fileidx := 0; + hdir := h.DirHash; + SetLength(fFoldersTES4, folderidx + 1); + fFoldersTES4[folderidx].Hash := h.DirHash; + fFoldersTES4[folderidx].Name := LowerCase(fdir); + // calc folder names length + Inc(fHeaderTES4.FolderNamesLength, Length(fdir) + 1); // + terminator only, length prefix is not counted + end; + SetLength(fFoldersTES4[folderidx].Files, fileidx + 1); + fFoldersTES4[folderidx].Files[fileidx].Hash := h.FileHash; + fFoldersTES4[folderidx].Files[fileidx].Name := LowerCase(fname); + fFoldersTES4[folderidx].Files[fileidx].PackingCompression := h.pc; + Inc(fileidx); + Inc(fFoldersTES4[folderidx].FileCount); + Inc(fHeaderTES4.FileCount); + // calculate file names length + Inc(fHeaderTES4.FileNamesLength, Length(fname) + 1); // + terminator + end; + fHeaderTES4.FolderCount := Length(fFoldersTES4); + + // calculate folders offsets + // at the end fDataOffset will hold the total size of header, folder and file records + // in other words the start of files data + fDataOffset := SizeOf(fMagic) + SizeOf(fVersion) + SizeOf(fHeaderTES4) + 16 * Length(fFoldersTES4); + // SSE folder record is 8 bytes larger + if fType = baSSE then + Inc(fDataOffset, 8 * Length(fFoldersTES4)); + // Offsets are stored including this value + Inc(fDataOffset, fHeaderTES4.FileNamesLength); + for i := Low(fFoldersTES4) to High(fFoldersTES4) do begin + fFoldersTES4[i].Offset := fDataOffset; + // add folder name length + Inc(fDataOffset, Length(fFoldersTES4[i].Name) + 2); // + length prefix + terminator + // add file records length + Inc(fDataOffset, 16 * Length(fFoldersTES4[i].Files)); + end; + + // final flags detection + + // misc file flag is not in Skyrim SE + if fType = baSSE then + fHeaderTES4.FileFlags := fHeaderTES4.FileFlags and not FILE_MISC; + + // embedded names in texture only archives + // except Skyrim SE: crashing engine bug if texture is uncompressed and file name is embedded + if (fHeaderTES4.FileFlags = FILE_DDS) and (fType <> baSSE) then + fHeaderTES4.Flags := fHeaderTES4.Flags or ARCHIVE_EMBEDNAME; + + // startupstr flag in archives with meshes + if fHeaderTES4.FileFlags and FILE_NIF <> 0 then + fHeaderTES4.Flags := fHeaderTES4.Flags or ARCHIVE_STARTUPSTR; + + // retainname flag in archives with sounds + if fHeaderTES4.FileFlags and FILE_WAV <> 0 then + fHeaderTES4.Flags := fHeaderTES4.Flags or ARCHIVE_RETAINNAME; + + // txt, xml and fnt file flags are exclusive for Oblivion + if fType <> baTES4 then + fHeaderTES4.FileFlags := fHeaderTES4.FileFlags and not (FILE_XML or FILE_TXT or FILE_FNT); + + // set compression flag if needed + if fCompress then + fHeaderTES4.Flags := fHeaderTES4.Flags or ARCHIVE_COMPRESS; + end + + else if fType in [baFO4, baFO4dds, baSF, baSFdds] then begin + if not Assigned(aFilesList) or (aFilesList.Count = 0) then + raise Exception.Create('Archive requires predefined files list'); + + fHeaderFO4.FileCount := aFilesList.Count; + SetLength(fFilesFO4, aFilesList.Count); + for i := 0 to Pred(aFilesList.Count) do begin + if SplitDirName(aFilesList[i], fdir, fname) = 0 then + raise Exception.Create('File is missing the folder part: ' + aFileName); + + SplitNameExt(fname, name, fext, True); + // archive2.exe uses / + fFilesFO4[i].Name := StringReplace(aFilesList[i], '\', '/', [rfReplaceAll]); + fFilesFO4[i].DirHash := CreateHashFO4(fdir); + fFilesFO4[i].NameHash := CreateHashFO4(name); + fFilesFO4[i].Ext := String2Magic(LowerCase(fext)); + fFilesFO4[i].Unknown := iFileFO4Unknown; + fFilesFO4[i].PackingCompression := TPackingCompression(aFilesList.Objects[i]); + end; + + fDataOffset := SizeOf(fMagic) + SizeOf(fVersion) + SizeOf(fHeaderFO4); + if fType = baSF then + Inc(fDataOffset, SizeOf(fHeaderSFv2)) + else if fType = baSFdds then + Inc(fDataOffset, SizeOf(fHeaderSFv3)); + + // file records have fixed length in general archive + if fType in [baFO4, baSF] then + fDataOffset := fDataOffset + 36 * Length(fFilesFO4) + + // variable file record length depending on DDS chunks number + else if fType in [baFO4dds, baSFdds] then begin + if not Assigned(fDDSInfoProc) then + raise Exception.Create('DDS archive requires DDS file information callback'); + + for i := 0 to Pred(aFilesList.Count) do begin + fDDSInfoProc(Self, aFilesList[i], ddsinfo, Self.fDDSInfoProcContext); + fDataOffset := fDataOffset + 24 {size of file record} + 24 {size of each texchunk} * GetDDSMipChunkNum(ddsinfo); + end; + end; + + end; + + fStream := TwbWriteCachedFileStream.Create(aFileName, fmCreate); + fFileName := aFileName; + Include(fStates, stWriting); + + // reserve space for the header + SetLength(Buffer, fDataOffset); + fStream.Write(Buffer[0], Length(Buffer)); +end; + +procedure TwbBSArchive.Save; +var + i, j: integer; +begin + if not (stWriting in fStates) then + raise Exception.Create('Archive is not in writing mode'); + + case fType of + baTES3: begin + for i := Low(fFilesTES3) to High(fFilesTES3) do + if fFilesTES3[i].Offset = 0 then + raise Exception.Create('Archived file has no data: ' + fFilesTES3[i].Name); + + // write header + fStream.Position := 0; + // magic, header record + fStream.Write(fMagic, SizeOf(fMagic)); + fStream.Write(fHeaderTES3, SizeOf(fHeaderTES3)); + // file sizes/offsets + for i := Low(fFilesTES3) to High(fFilesTES3) do begin + fStream.WriteCardinal(fFilesTES3[i].Size); + fStream.WriteCardinal(fFilesTES3[i].Offset - fDataOffset); // offsets are relative + end; + // Archive directory/name offsets + j := 0; + for i := Low(fFilesTES3) to High(fFilesTES3) do begin + fStream.WriteCardinal(j); + Inc(j, Length(fFilesTES3[i].Name) + 1); // including terminator + end; + // Filename records + for i := Low(fFilesTES3) to High(fFilesTES3) do + fStream.WriteStringTerm(fFilesTES3[i].Name); + // Hash table + for i := Low(fFilesTES3) to High(fFilesTES3) do begin + fStream.WriteCardinal(fFilesTES3[i].Hash shr 32); + fStream.WriteCardinal(fFilesTES3[i].Hash and $FFFFFFFF); + end; + end; + + baTES4, baFO3, baSSE: begin + // check that all files from files table have saved data + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do + if fFoldersTES4[i].Files[j].Offset = 0 then + raise Exception.Create('Archived file has no data: ' + fFoldersTES4[i].Name + '\' + fFoldersTES4[i].Files[j].Name); + + // write header + fStream.Position := 0; + // magic, version, header record + fStream.Write(fMagic, SizeOf(fMagic)); + fStream.Write(fVersion, SizeOf(fVersion)); + fStream.Write(fHeaderTES4, SizeOf(fHeaderTES4)); + // folder records + for i := Low(fFoldersTES4) to High(fFoldersTES4) do begin + fStream.WriteUInt64(fFoldersTES4[i].Hash); + fStream.WriteCardinal(fFoldersTES4[i].FileCount); + if fType = baSSE then begin + fStream.WriteCardinal(fFoldersTES4[i].Unk32); + fStream.WriteInt64(fFoldersTES4[i].Offset); + end else + fStream.WriteCardinal(fFoldersTES4[i].Offset); + end; + // file records + for i := Low(fFoldersTES4) to High(fFoldersTES4) do begin + fStream.WriteStringLen(fFoldersTES4[i].Name); + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do begin + fStream.WriteUInt64(fFoldersTES4[i].Files[j].Hash); + fStream.WriteCardinal(fFoldersTES4[i].Files[j].Size); + fStream.WriteCardinal(fFoldersTES4[i].Files[j].Offset); + end; + end; + // file names + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do + fStream.WriteStringTerm(fFoldersTES4[i].Files[j].Name); + end; + + baFO4, baSF: begin + for i := Low(fFilesFO4) to High(fFilesFO4) do + if fFilesFO4[i].Offset = 0 then + raise Exception.Create('Archived file has no data: ' + fFilesFO4[i].Name); + + // file names table + fHeaderFO4.FileTableOffset := fStream.Position; + for i := Low(fFilesFO4) to High(fFilesFO4) do + fStream.WriteStringLen16(fFilesFO4[i].Name); + // write header + fStream.Position := 0; + // magic, version, header record + fStream.Write(fMagic, SizeOf(fMagic)); + fStream.Write(fVersion, SizeOf(fVersion)); + fStream.Write(fHeaderFO4, SizeOf(fHeaderFO4)); + // additional SF header + if fType = baSF then begin + fHeaderSFv2.Unknown1 := 1; + fHeaderSFv2.Unknown2 := 0; + fStream.Write(fHeaderSFv2, SizeOf(fHeaderSFv2)); + end; + // file records + for i := Low(fFilesFO4) to High(fFilesFO4) do begin + fStream.WriteCardinal(fFilesFO4[i].NameHash); + fStream.WriteBuffer(fFilesFO4[i].Ext[0], SizeOf(fFilesFO4[i].Ext)); + fStream.WriteCardinal(fFilesFO4[i].DirHash); + fStream.WriteCardinal(fFilesFO4[i].Unknown); + fStream.WriteInt64(fFilesFO4[i].Offset); + fStream.WriteCardinal(fFilesFO4[i].PackedSize); + fStream.WriteCardinal(fFilesFO4[i].Size); + fStream.WriteCardinal(iFileFO4Tail); + end; + end; + + baFO4dds, baSFdds: begin + for i := Low(fFilesFO4) to High(fFilesFO4) do + if Length(fFilesFO4[i].TexChunks) = 0 then + raise Exception.Create('Archived file has no data: ' + fFilesFO4[i].Name); + + // file names table + fHeaderFO4.FileTableOffset := fStream.Position; + for i := Low(fFilesFO4) to High(fFilesFO4) do + fStream.WriteStringLen16(fFilesFO4[i].Name); + // write header + fStream.Position := 0; + // magic, version, header record + fStream.Write(fMagic, SizeOf(fMagic)); + fStream.Write(fVersion, SizeOf(fVersion)); + fStream.Write(fHeaderFO4, SizeOf(fHeaderFO4)); + // additional SF header + if fType = baSFdds then begin + fHeaderSFv3.Unknown1 := 1; + fHeaderSFv3.Unknown2 := 0; + fHeaderSFv3.CompressionMethod := 3; // lz4 + fStream.Write(fHeaderSFv3, SizeOf(fHeaderSFv3)); + end; + // file records + for i := Low(fFilesFO4) to High(fFilesFO4) do begin + fStream.WriteCardinal(fFilesFO4[i].NameHash); + fStream.WriteBuffer(fFilesFO4[i].Ext[0], SizeOf(fFilesFO4[i].Ext)); + fStream.WriteCardinal(fFilesFO4[i].DirHash); + fStream.WriteByte(fFilesFO4[i].UnknownTex); + fStream.WriteByte(Length(fFilesFO4[i].TexChunks)); + fStream.WriteWord(24); // fixed chunk header size + fStream.WriteWord(fFilesFO4[i].Height); + fStream.WriteWord(fFilesFO4[i].Width); + fStream.WriteByte(fFilesFO4[i].NumMips); + fStream.WriteByte(fFilesFO4[i].DXGIFormat); + fStream.WriteWord(fFilesFO4[i].CubeMaps); + for j := Low(fFilesFO4[i].TexChunks) to High(fFilesFO4[i].TexChunks) do + with fFilesFO4[i].TexChunks[j] do begin + fStream.WriteUInt64(Offset); + fStream.WriteCardinal(PackedSize); + fStream.WriteCardinal(Size); + fStream.WriteWord(StartMip); + fStream.WriteWord(EndMip); + fStream.WriteCardinal(iFileFO4Tail); + end; + end; + end; + + end; + + FreeAndNil(fStream); + Exclude(fStates, stWriting); + Close; +end; + +function TwbBSArchive.GetCreatedArchiveSize: Int64; +begin + if (stWriting in fStates) and Assigned(fStream) then + Result := fStream.Position + else + Result := 0; +end; + +procedure TwbBSArchive.AddFileDisk(const aFilePath, aSourcePath: string); +var + fname: string; + i: integer; + buffer: PByte; + stream: TFileStream; +begin + if not (stWriting in fStates) then + raise Exception.Create('Archive is not in writing mode'); + + stream := TFileStream.Create(aSourcePath, fmOpenRead + fmShareDenyNone); + try + // Modified: Make sure memory is zeroed when allocated + buffer := AllocMem(stream.Size); + try + stream.Read(buffer^, stream.Size); + AddFileData(aFilePath, stream.Size, buffer); + finally + if Assigned(buffer) then + FreeMem(buffer); + end; + finally + stream.Free; + end; +end; + +procedure TwbBSArchive.AddFileDiskRoot(const aRootDir, aSourcePath: string); +var + fname: string; + i: integer; +begin + i := Length(aRootDir); + if (i > 1) and (aRootDir[Length(aRootDir)] <> '\') then + Inc(i); + + fname := Copy(aSourcePath, i + 1, Length(aSourcePath)); + + AddFileDisk(fname, aSourcePath); +end; + +procedure TwbBSArchive.SyncBeginWrite; +begin + if fMultiThreaded then + Sync.BeginWrite; +end; + +procedure TwbBSArchive.SyncEndWrite; +begin + if fMultiThreaded then + Sync.EndWrite; +end; + +procedure TwbBSArchive.CompressStream(aSrc, aDst: TStream); +begin + case fCompressionType of + ctZlib: ZCompressStream(aSrc, aDst); + ctLZ4Frame: lz4CompressStream(aSrc, aDst); + ctLZ4Block: lz4BlockCompressStream(aSrc, aDst); + else + raise Exception.Create('Archive compression type is undefined'); + end; +end; + +procedure TwbBSArchive.DecompressBuf(aSrc: Pointer; aSrcSize: Integer; aDst: Pointer; aDstSize: Integer); +begin + case fCompressionType of + ctZlib: try + DecompressToUserBuf(aSrc, aSrcSize, aDst, aDstSize); + except + // ignore zlib's Buffer error since it happens in vanilla "Fallout - Misc.bsa" + // Bethesda probably used old buggy zlib version when packing it + on E: Exception do if E.Message <> 'Buffer error' then raise; + end; + ctLZ4Frame: lz4DecompressToUserBuf(aSrc, aSrcSize, aDst, aDstSize); + ctLZ4Block: lz4BlockDecompressToUserBuf(aSrc, aSrcSize, aDst, aDstSize); + end; +end; + +procedure TwbBSArchive.PackData(aFileRecord: Pointer; const aFileName: string; + aDataHash: TPackedDataHash; aData: PByte; aSize: Integer; + aCompress: Boolean; aDoCompress: Boolean = False); +var + zStream: TBytesStream; + msData: TPreallocatedMemoryStream; + DataSize: Integer; + Position: Int64; +begin + DataSize := aSize; + zStream := nil; + msData := nil; + + if FindPackedData(DataSize, aDataHash, aFileRecord) then + Exit; + + try + if aCompress then begin + // compressing in parallel when multithreaded + SyncEndWrite; + try + zStream := TBytesStream.Create; + msData := TPreallocatedMemoryStream.Create(aData, aSize); + CompressStream(msData, zStream); + + // leave as compressed if compression reduced the size + // by at least let's say 32 bytes + // or data is forced to be compressed + if aDoCompress or (zStream.Size + 32 < aSize) then begin + aData := @zStream.Bytes[0]; + aSize := zStream.Size; + end else + aCompress := False; + finally + SyncBeginWrite; + end; + end; + + // let's try to find existing data again if multithreaded + // maybe some other thread has written the same data while we've been busy compressing + // zStream exists if we've really spent time compressing + if fMultiThreaded and Assigned(zStream) then + if FindPackedData(DataSize, aDataHash, aFileRecord) then + Exit; + + Position := fStream.Position; + + // embedded name for Fallout 3/NV/Skyrim/Skyrim SE + if (fType in [baFO3, baSSE]) and (fHeaderTES4.Flags and ARCHIVE_EMBEDNAME <> 0) then + fStream.WriteStringLen(aFileName, False); + + // if compressed then write uncompressed size first for Oblivion/Fallout 3/NV/Skyrim/Skyrim SE + if (fType in [baTES4, baFO3, baSSE]) and aCompress then + fStream.WriteCardinal(DataSize); + + fStream.Write(aData^, aSize); + + // updating file record + case fType of + baTES3: with PwbBSFileTES3(aFileRecord)^ do begin + Offset := Position; + Size := DataSize; + end; + baTES4, baFO3, baSSE: with PwbBSFileTES4(aFileRecord)^ do begin + Offset := Position; + Size := fStream.Position - Offset; + // compress flag in Size inverts compression status from the header + // set it if archive's compression doesn't match file's compression + if Self.fCompress xor aCompress then + Size := Size or FILE_SIZE_COMPRESS; + end; + baFO4, baSF: with PwbBSFileFO4(aFileRecord)^ do begin + Offset := Position; + Size := DataSize; + if aCompress then + PackedSize := aSize; + end; + baFO4dds, baSFdds: with PwbBSTexChunkRec(aFileRecord)^ do begin + Offset := Position; + Size := DataSize; + if aCompress then + PackedSize := aSize; + end; + end; + + AddPackedData(DataSize, aDataHash, aFileRecord); + + finally + if Assigned(zStream) then + zStream.Free; + if Assigned(msData) then + msData.Free; + end; +end; + +procedure TwbBSArchive.AddFileData(const aFileName: string; const aSize: Cardinal; const aData: PByte); +var + i, j, Off, MipSize, BitsPerPixel: integer; + DataHash: TPackedDataHash; + DDSHeader: PDDSHeader; + DDSHeaderDX10: PDDSHeaderDX10; + DDSInfo: TDDSInfo; +begin + if not (stWriting in fStates) then + raise Exception.Create('Archive is not in writing mode'); + + // dds mipmaps have their own partial hash calculation down below + if fShareData and not (fType in [baFO4dds, baSFdds]) then + DataHash := CalcDataHash(@aData[0], aSize); + + SyncBeginWrite; + try + case fType of + baTES3: begin + if not FindFileRecordTES3(aFileName, i) then + raise Exception.Create('File not found in files table: ' + aFileName); + + PackData(@fFilesTES3[i], aFileName, DataHash, @aData[0], aSize, False); + end; + + baTES4, baFO3, baSSE: begin + if not FindFileRecordTES4(aFileName, i, j) then + raise Exception.Create('File not found in files table: ' + aFileName); + + PackData( + @fFoldersTES4[i].Files[j], fFoldersTES4[i].Name + '\' + fFoldersTES4[i].Files[j].Name, + DataHash, @aData[0], aSize, fFoldersTES4[i].Files[j].Compress(Self) + ); + end; + + baFO4, baSF: begin + if not FindFileRecordFO4(aFileName, i) then + raise Exception.Create('File not found in files table: ' + aFileName); + + fFilesFO4[i].Offset := fStream.Position; + fFilesFO4[i].Size := aSize; + + PackData( + @fFilesFO4[i], fFilesFO4[i].Name, + DataHash, @aData[0], aSize, fFilesFO4[i].Compress(Self) + ); + end; + + baFO4dds, baSFdds: begin + if not FindFileRecordFO4(aFileName, i) then + raise Exception.Create('File not found in files table: ' + aFileName); + + fFilesFO4[i].UnknownTex := 0; + // DDS file parameters + DDSHeader := @aData[0]; + Off := SizeOf(DDSHeader^); // offset to image data + fFilesFO4[i].Width := DDSHeader.dwWidth; + fFilesFO4[i].Height := DDSHeader.dwHeight; + fFilesFO4[i].NumMips := DDSHeader.dwMipMapCount; + // no mipmaps is equal to a single one + if fFilesFO4[i].NumMips = 0 then + fFilesFO4[i].NumMips := 1; + + // DXGI detection + if DDSHeader.ddspf.dwFourCC = MAGIC_DXT1 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC1_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_DXT3 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC2_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_DXT5 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC3_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_ATI1 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC4_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_BC4U then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC4_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_BC4S then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC4_SNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_ATI2 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC5_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_BC5U then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC5_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_BC5S then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC5_SNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_DX10 then begin + DDSHeaderDX10 := @aData[Off]; + Off := Off + SizeOf(DDSHeaderDX10^); + fFilesFO4[i].DXGIFormat := Byte(DDSHeaderDX10.dxgiFormat); + end + else begin + if DDSHeader.ddspf.dwRGBBitCount = 32 then + if DDSHeader.ddspf.dwFlags and DDPF_ALPHAPIXELS = 0 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_B8G8R8X8_UNORM) + else if DDSHeader.ddspf.dwRBitMask = $000000FF then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_R8G8B8A8_UNORM) + else + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_B8G8R8A8_UNORM) + + else if DDSHeader.ddspf.dwRGBBitCount = 16 then + if (DDSHeader.ddspf.dwRBitMask = $F800) and (DDSHeader.ddspf.dwGBitMask = $07E0) and + (DDSHeader.ddspf.dwBBitMask = $001F) and (DDSHeader.ddspf.dwABitMask = $0000) then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_B5G6R5_UNORM) + else if (DDSHeader.ddspf.dwRBitMask = $7C00) and (DDSHeader.ddspf.dwGBitMask = $03E0) and + (DDSHeader.ddspf.dwBBitMask = $001F) and (DDSHeader.ddspf.dwABitMask = $8000) then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_B5G5R5A1_UNORM) + else + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_R8G8_UNORM) + + else if DDSHeader.ddspf.dwRGBBitCount = 8 then + if DDSHeader.ddspf.dwFlags and DDPF_ALPHA <> 0 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_A8_UNORM) + else + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_R8_UNORM) + + else + raise Exception.Create('Unsupported uncompressed DDS format'); + end; + + // MipMap size detection + case TDXGI(fFilesFO4[i].DXGIFormat) of + DXGI_FORMAT_BC1_UNORM, DXGI_FORMAT_BC1_UNORM_SRGB, + DXGI_FORMAT_BC4_UNORM, DXGI_FORMAT_BC4_SNORM: + BitsPerPixel := 4; + DXGI_FORMAT_BC2_UNORM, DXGI_FORMAT_BC2_UNORM_SRGB, + DXGI_FORMAT_BC3_UNORM, DXGI_FORMAT_BC3_UNORM_SRGB, + DXGI_FORMAT_BC5_UNORM, DXGI_FORMAT_BC5_SNORM, + DXGI_FORMAT_BC6H_SF16, DXGI_FORMAT_BC6H_UF16, + DXGI_FORMAT_BC7_UNORM, DXGI_FORMAT_BC7_UNORM_SRGB, + DXGI_FORMAT_A8_UNORM, + DXGI_FORMAT_R8_SINT, DXGI_FORMAT_R8_SNORM, + DXGI_FORMAT_R8_UINT, DXGI_FORMAT_R8_UNORM: + BitsPerPixel := 8; + DXGI_FORMAT_B5G6R5_UNORM, DXGI_FORMAT_B5G5R5A1_UNORM, + DXGI_FORMAT_R8G8_SINT, DXGI_FORMAT_R8G8_UINT, DXGI_FORMAT_R8G8_UNORM: + BitsPerPixel := 16; + DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, + DXGI_FORMAT_B8G8R8X8_UNORM, DXGI_FORMAT_B8G8R8X8_UNORM_SRGB, + DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R8G8B8A8_SINT, + DXGI_FORMAT_R8G8B8A8_UINT, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + BitsPerPixel := 32; + else + raise Exception.Create('Unsupported DDS format'); + end; + MipSize := (fFilesFO4[i].Width * fFilesFO4[i].Height * BitsPerPixel) shr 3; + + // cubemaps detection + fFilesFO4[i].CubeMaps := $800; + if DDSHeader.dwCaps2 and DDSCAPS2_CUBEMAP <> 0 then + fFilesFO4[i].CubeMaps := fFilesFO4[i].CubeMaps or 1; + + // number of chunks to store in file record + DDSInfo.Width := fFilesFO4[i].Width; + DDSInfo.Height := fFilesFO4[i].Height; + DDSInfo.MipMaps := fFilesFO4[i].NumMips; + SetLength(fFilesFO4[i].TexChunks, GetDDSMipChunkNum(DDSInfo)); + + // storing chunks + for j := Low(fFilesFO4[i].TexChunks) to High(fFilesFO4[i].TexChunks) do begin + fFilesFO4[i].TexChunks[j].StartMip := j; + if j < High(fFilesFO4[i].TexChunks) then + fFilesFO4[i].TexChunks[j].EndMip := j + else begin + // last chunk stores all remaining mipmaps + fFilesFO4[i].TexChunks[j].EndMip := Pred(fFilesFO4[i].NumMips); + MipSize := Integer(aSize) - Off; + end; + + DataHash := CalcDataHash(@aData[Off], MipSize); + + PackData( + @fFilesFO4[i].TexChunks[j], fFilesFO4[i].Name, + DataHash, @aData[Off], MipSize, + fFilesFO4[i].Compress(Self), fFilesFO4[i].Compress(Self) // force compression + ); + Inc(Off, MipSize); + MipSize := MipSize div 4; + end; + end; + + end; + finally + SyncEndWrite; + end; +end; + +function TwbBSArchive.ExtractFileData(aFileRecord: Pointer): TwbBSResultBuffer; +var + FileTES3: PwbBSFileTES3; + FileTES4: PwbBSFileTES4; + FileFO4: PwbBSFileFO4; + DDSHeader: PDDSHeader; + DDSHeaderDX10: PDDSHeaderDX10; + i, size, TexSize: integer; + bCompressed: Boolean; + Buffer: TBytes; +begin + if not (stReading in fStates) then + raise Exception.Create('Archive is not loaded'); + + if aFileRecord = nil then + Exit; + + SyncBeginWrite; + try + case fType of + baTES3: begin + FileTES3 := aFileRecord; + fStream.Position := fDataOffset + FileTES3.Offset; + Result.size := FileTES3.Size; + // Modified: Make sure memory is zeroed when allocated + Result.data := AllocMem(FileTES3.Size); + end; + + baTES4, baFO3, baSSE: begin + FileTES4 := aFileRecord; + fStream.Position := FileTES4.Offset; + size := FileTES4.Size; + bCompressed := size and FILE_SIZE_COMPRESS <> 0; + if bCompressed then + size := size and not FILE_SIZE_COMPRESS; + if fHeaderTES4.Flags and ARCHIVE_COMPRESS <> 0 then + bCompressed := not bCompressed; + + // skip embedded file name + length prefix + if (fType in [baFO3, baSSE]) and (fHeaderTES4.Flags and ARCHIVE_EMBEDNAME <> 0) then + size := size - (Length(fStream.ReadStringLen(False)) + 1); + + if bCompressed then begin + // reading uncompressed size + Result.size := fStream.ReadCardinal; + // Modified: Make sure memory is zeroed when allocated + Result.data := AllocMem(Result.size); + dec(size, SizeOf(Cardinal)); + if (Result.size > 0) and (size > 0) then begin + SetLength(Buffer, size); + fStream.ReadBuffer(Buffer[0], Length(Buffer)); + SyncEndWrite; + try + DecompressBuf(@Buffer[0], Length(Buffer), @Result.data[0], Result.size); + finally + SyncBeginWrite; + end; + end; + end + else begin + Result.size := size; + // Modified: Make sure memory is zeroed when allocated + Result.data := AllocMem(Result.size); + if size > 0 then + fStream.ReadBuffer(Result.data[0], Result.size); + end; + end; + + baFO4, baSF: begin + FileFO4 := aFileRecord; + fStream.Position := FileFO4.Offset; + if FileFO4.PackedSize <> 0 then begin + SetLength(Buffer, FileFO4.PackedSize); + fStream.ReadBuffer(Buffer[0], Length(Buffer)); + Result.size := FileFO4.Size; + // Modified: Make sure memory is zeroed when allocated + Result.data := AllocMem(Result.size); + SyncEndWrite; + try + DecompressBuf(@Buffer[0], Length(Buffer), @Result.data[0], Result.size); + finally + SyncBeginWrite; + end; + end + else begin + Result.size := FileFO4.Size; + // Modified: Make sure memory is zeroed when allocated + Result.data := AllocMem(Result.size); + fStream.ReadBuffer(Result.data[0], Result.size); + end; + end; + + baFO4dds, baSFdds: begin + FileFO4 := aFileRecord; + + TexSize := SizeOf(TDDSHeader); + for i := Low(FileFO4.TexChunks) to High(FileFO4.TexChunks) do + Inc(TexSize, FileFO4.TexChunks[i].Size); + Result.size := Texsize; + + // Modified: Make sure memory is zeroed when allocated + Result.data := AllocMem(Result.size); + + DDSHeader := @Result.data[0]; + DDSHeader.Magic := MAGIC_DDS; + DDSHeader.dwSize := SizeOf(TDDSHeader) - SizeOf(TMagic4); + DDSHeader.dwWidth := FileFO4.Width; + DDSHeader.dwHeight := FileFO4.Height; + DDSHeader.dwFlags := DDSD_CAPS or DDSD_PIXELFORMAT or + DDSD_WIDTH or DDSD_HEIGHT or DDSD_MIPMAPCOUNT; + DDSHeader.dwCaps := DDSCAPS_TEXTURE; + DDSHeader.dwMipMapCount := FileFO4.NumMips; + if DDSHeader.dwMipMapCount > 1 then + DDSHeader.dwCaps := DDSHeader.dwCaps or DDSCAPS_MIPMAP or DDSCAPS_COMPLEX; + DDSHeader.dwDepth := 1; + + DDSHeaderDX10 := @Result.data[SizeOf(TDDSHeader)]; + DDSHeaderDX10.resourceDimension := DDS_DIMENSION_TEXTURE2D; + DDSHeaderDX10.arraySize := 1; + + if FileFO4.CubeMaps = 2049 then begin + // Archive2.exe creates invalid textures like this + //DDSHeader.dwCaps := DDSHeader.dwCaps or DDSCAPS2_CUBEMAP or DDSCAPS_COMPLEX + // or DDSCAPS2_POSITIVEX or DDSCAPS2_NEGATIVEX + // or DDSCAPS2_POSITIVEY or DDSCAPS2_NEGATIVEY + // or DDSCAPS2_POSITIVEZ or DDSCAPS2_NEGATIVEZ; + // This is the correct way + DDSHeader.dwCaps := DDSHeader.dwCaps or DDSCAPS_COMPLEX; + DDSHeader.dwCaps2 := DDSCAPS2_CUBEMAP + or DDSCAPS2_POSITIVEX or DDSCAPS2_NEGATIVEX + or DDSCAPS2_POSITIVEY or DDSCAPS2_NEGATIVEY + or DDSCAPS2_POSITIVEZ or DDSCAPS2_NEGATIVEZ; + DDSHeaderDX10.miscFlags := DDS_RESOURCE_MISC_TEXTURECUBE; + end; + DDSHeader.ddspf.dwSize := SizeOf(DDSHeader.ddspf); + + case TDXGI(FileFO4.DXGIFormat) of + DXGI_FORMAT_BC1_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DXT1; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height div 2; + end; + DXGI_FORMAT_BC2_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DXT3; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height; + end; + DXGI_FORMAT_BC3_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DXT5; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height; + end; + DXGI_FORMAT_BC4_SNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_BC4S; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height div 2; + end; + DXGI_FORMAT_BC4_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_BC4U; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height div 2; + end; + DXGI_FORMAT_BC5_SNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_BC5S; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height; + end; + DXGI_FORMAT_BC5_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_BC5U; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height; + end; + DXGI_FORMAT_BC1_UNORM_SRGB: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DX10; + DDSHeaderDX10.dxgiFormat := Integer(FileFO4.DXGIFormat); + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height div 2; + end; + DXGI_FORMAT_BC2_UNORM_SRGB, DXGI_FORMAT_BC3_UNORM_SRGB, + DXGI_FORMAT_BC6H_UF16, DXGI_FORMAT_BC6H_SF16, + DXGI_FORMAT_BC7_UNORM, DXGI_FORMAT_BC7_UNORM_SRGB: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DX10; + DDSHeaderDX10.dxgiFormat := Integer(FileFO4.DXGIFormat); + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height; + end; + DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, DXGI_FORMAT_B8G8R8X8_UNORM_SRGB, + DXGI_FORMAT_R8G8B8A8_SINT, DXGI_FORMAT_R8G8B8A8_UINT, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DX10; + DDSHeaderDX10.dxgiFormat := Integer(FileFO4.DXGIFormat); + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 4; + end; + DXGI_FORMAT_R8G8_SINT, DXGI_FORMAT_R8G8_UINT: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DX10; + DDSHeaderDX10.dxgiFormat := Integer(FileFO4.DXGIFormat); + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 2; + end; + DXGI_FORMAT_R8_SINT, DXGI_FORMAT_R8_SNORM, DXGI_FORMAT_R8_UINT: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DX10; + DDSHeaderDX10.dxgiFormat := Integer(FileFO4.DXGIFormat); + DDSHeader.dwPitchOrLinearSize := FileFO4.Width; + end; + DXGI_FORMAT_R8G8B8A8_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_RGB or DDPF_ALPHAPIXELS; + DDSHeader.ddspf.dwRGBBitCount := 32; + DDSHeader.ddspf.dwRBitMask := $000000FF; + DDSHeader.ddspf.dwGBitMask := $0000FF00; + DDSHeader.ddspf.dwBBitMask := $00FF0000; + DDSHeader.ddspf.dwABitMask := $FF000000; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 4; + end; + DXGI_FORMAT_B8G8R8A8_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_RGB or DDPF_ALPHAPIXELS; + DDSHeader.ddspf.dwRGBBitCount := 32; + DDSHeader.ddspf.dwRBitMask := $00FF0000; + DDSHeader.ddspf.dwGBitMask := $0000FF00; + DDSHeader.ddspf.dwBBitMask := $000000FF; + DDSHeader.ddspf.dwABitMask := $FF000000; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 4; + end; + DXGI_FORMAT_B8G8R8X8_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_RGB; + DDSHeader.ddspf.dwRGBBitCount := 32; + DDSHeader.ddspf.dwRBitMask := $00FF0000; + DDSHeader.ddspf.dwGBitMask := $0000FF00; + DDSHeader.ddspf.dwBBitMask := $000000FF; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 4; + end; + DXGI_FORMAT_B5G6R5_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_RGB; + DDSHeader.ddspf.dwRGBBitCount := 16; + DDSHeader.ddspf.dwRBitMask := $0000F800; + DDSHeader.ddspf.dwGBitMask := $000007E0; + DDSHeader.ddspf.dwBBitMask := $0000001F; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 2; + end; + DXGI_FORMAT_B5G5R5A1_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_RGB or DDPF_ALPHAPIXELS; + DDSHeader.ddspf.dwRGBBitCount := 16; + DDSHeader.ddspf.dwRBitMask := $00007C00; + DDSHeader.ddspf.dwGBitMask := $000003E0; + DDSHeader.ddspf.dwBBitMask := $0000001F; + DDSHeader.ddspf.dwABitMask := $00008000; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 2; + end; + DXGI_FORMAT_R8G8_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_LUMINANCE OR DDPF_ALPHAPIXELS; + DDSHeader.ddspf.dwRGBBitCount := 16; + DDSHeader.ddspf.dwRBitMask := $000000FF; + DDSHeader.ddspf.dwABitMask := $0000FF00; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 2; + end; + DXGI_FORMAT_A8_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_ALPHA; + DDSHeader.ddspf.dwRGBBitCount := 8; + DDSHeader.ddspf.dwABitMask := $000000FF; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width; + end; + DXGI_FORMAT_R8_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_LUMINANCE; + DDSHeader.ddspf.dwRGBBitCount := 8; + DDSHeader.ddspf.dwRBitMask := $000000FF; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width; + end; + end; + + TexSize := SizeOf(TDDSHeader); + if DDSHeader.ddspf.dwFourCC = MAGIC_DX10 then begin + ReallocMem(Result.data, Result.size + SizeOf(TDDSHeaderDX10)); + Inc(TexSize, SizeOf(TDDSHeaderDX10)); + Result.size := TexSize; + end; + + // append chunks + for i := Low(FileFO4.TexChunks) to High(FileFO4.TexChunks) do with FileFO4.TexChunks[i] do begin + fStream.Position := Offset; + // compressed chunk + if PackedSize <> 0 then begin + SetLength(Buffer, PackedSize); + fStream.ReadBuffer(Buffer[0], Length(Buffer)); + SyncEndWrite; + try + DecompressBuf(@Buffer[0], Length(Buffer), @Result.data[TexSize], Size); + finally + SyncBeginWrite; + end; + end + // uncompressed chunk + else + fStream.ReadBuffer(Result.data[TexSize], Size); + + Inc(TexSize, Size); + Result.size := TexSize; + end; + end + + else + raise Exception.Create('Extraction is not supported for this archive'); + end; + finally + SyncEndWrite; + end; +end; + +function TwbBSArchive.ExtractFileData(const aFileName: string): TwbBSResultBuffer; +var + FileRecord: Pointer; +begin + if not (stReading in fStates) then + raise Exception.Create('Archive is not loaded'); + + FileRecord := FindFileRecord(aFileName); + + if not Assigned(FileRecord) then + raise Exception.Create('File not found in archive'); + + Result := ExtractFileData(FileRecord); +end; + +// Addded: For use in non-Borland C/C++ +procedure TwbBSArchive.ReleaseFileData(fileDataResult: TwbBSResultBuffer); +begin + FreeMem(fileDataResult.data); + fileDataResult.size := 0; +end; + +procedure TwbBSArchive.ExtractFile(const aFileName, aSaveAs: string); +var + fs: TFileStream; + fileData: TwbBSResultBuffer; +begin + if not (stReading in fStates) then + raise Exception.Create('Archive is not loaded'); + + fs := TFileStream.Create(aSaveAs, fmCreate); + try + fileData := ExtractFileData(aFileName); + fs.Write(fileData.data[0], fileData.size); + finally + ReleaseFileData(fileData); + fs.Free; + end; +end; + +procedure TwbBSArchive.IterateFiles(aProc: TBSFileIterationProc; aData: Pointer = nil; + aSingleThreaded: Boolean = False); +var + i, j: Integer; +begin + if not Assigned(aProc) then + Exit; + + if fMultiThreaded and not aSingleThreaded then + case fType of + baTES3: + TParallel.&For(Low(fFilesTES3), High(fFilesTES3), procedure(i: Integer; LoopState: TParallel.TLoopState) begin + if aProc(Self, fFilesTES3[i].Name, @fFilesTES3[i], nil, aData) then + LoopState.Stop; + end); + baTES4, baFO3, baSSE: + TParallel.&For(Low(fFoldersTES4), High(fFoldersTES4), procedure(i: Integer; OuterLoopState: TParallel.TLoopState) begin + TParallel.&For(Low(fFoldersTES4[i].Files), High(fFoldersTES4[i].Files), procedure(j: Integer; InnerLoopState: TParallel.TLoopState) begin + if aProc(Self, fFoldersTES4[i].Name + '\' + fFoldersTES4[i].Files[j].Name, @fFoldersTES4[i].Files[j], @fFoldersTES4[i], aData) then begin + OuterLoopState.Stop; + InnerLoopState.Stop; + end; + end); + end); + baFO4, baFO4dds, baSF, baSFdds: + TParallel.&For(Low(fFilesFO4), High(fFilesFO4), procedure(i: Integer; LoopState: TParallel.TLoopState) begin + if aProc(Self, fFilesFO4[i].Name, @fFilesFO4[i], nil, aData) then + LoopState.Stop; + end); + end + else + case fType of + baTES3: + for i := Low(fFilesTES3) to High(fFilesTES3) do + if aProc(Self, fFilesTES3[i].Name, @fFilesTES3[i], nil, aData) then + Break; + baTES4, baFO3, baSSE: + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do + if aProc(Self, fFoldersTES4[i].Name + '\' + fFoldersTES4[i].Files[j].Name, @fFoldersTES4[i].Files[j], @fFoldersTES4[i], aData) then + Break; + baFO4, baFO4dds, baSF, baSFdds: + for i := Low(fFilesFO4) to High(fFilesFO4) do + if aProc(Self, fFilesFO4[i].Name, @fFilesFO4[i], nil, aData) then + Break; + end; +end; + +{procedure TwbBSArchive.IterateFolders(aProc: TBSFileIterationProc); +var + i: Integer; +begin + if not Assigned(aProc) then + Exit; + + if fType in [baTES4, baFO3, baSSE] then + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + aProc(Self, fFoldersTES4[i].Name, nil, @fFoldersTES4[i]); +end;} + +function TwbBSArchive.FileExists(const aFileName: string): Boolean; +begin + Result := Assigned(FindFileRecord(aFileName)); +end; + +procedure TwbBSArchive.Close; +begin + if Assigned(fStream) then + FreeAndNil(fStream); + + if stWriting in fStates then + System.SysUtils.DeleteFile(fFileName); + + fStates := []; + fType := baNone; + fFileName := ''; + fDataOffset := 0; + FillChar(fHeaderTES3, SizeOf(fHeaderTES3), 0); + SetLength(fFilesTES3, 0); + FillChar(fHeaderTES4, SizeOf(fHeaderTES4), 0); + SetLength(fFoldersTES4, 0); + FillChar(fHeaderFO4, SizeOf(fHeaderFO4), 0); + SetLength(fFilesFO4, 0); + + if fShareData then begin + SetLength(fPackedData, 0); + fPackedDataCount := 0; + end; +end; + +procedure TwbBSArchive.ResourceDict(const aDict: TwbResourceDict; aFolder: string); +var + Folder : string; + i, j : Integer; +begin + if not Assigned(aDict) then + Exit; + + Folder := ExcludeTrailingPathDelimiter(aFolder); + case fType of + baTES3: + for i := Low(fFilesTES3) to High(fFilesTES3) do + with fFilesTES3[i] do + if (Folder = '') or Name.StartsWith(Folder, True) then + aDict.TryAdd(Name, wbNothing); + baTES4, baFO3, baSSE: + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + with fFoldersTES4[i] do begin + if (Folder = '') or Name.StartsWith(Folder, True) then + for j := Low(Files) to High(Files) do begin + var lName := Name + '\' + Files[j].Name; + aDict.TryAdd(lName, wbNothing); + end; + end; + baFO4, baFO4dds, baSF, baSFdds: + for i := Low(fFilesFO4) to High(fFilesFO4) do + with fFilesFO4[i] do + if (Folder = '') or Name.StartsWith(Folder, True) then + aDict.TryAdd(Name, wbNothing); + end; +end; + +procedure TwbBSArchive.ResourceList(const aList: TStrings; aFolder: string = ''); +var + Folder : string; + i, j : Integer; +begin + if not Assigned(aList) then + Exit; + + Folder := ExcludeTrailingPathDelimiter(aFolder); + case fType of + baTES3: + for i := Low(fFilesTES3) to High(fFilesTES3) do + with fFilesTES3[i] do + if (Folder = '') or Name.StartsWith(Folder, True) then + aList.Add(Name); + baTES4, baFO3, baSSE: + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + with fFoldersTES4[i] do begin + if (Folder = '') or Name.StartsWith(Folder, True) then + for j := Low(Files) to High(Files) do + aList.Add(Name + '\' + Files[j].Name); + end; + baFO4, baFO4dds, baSF, baSFdds: + for i := Low(fFilesFO4) to High(fFilesFO4) do + with fFilesFO4[i] do + if (Folder = '') or Name.StartsWith(Folder, True) then + aList.Add(Name); + end; + +end; + +end. diff --git a/libs/libbsarch/examples/libbsarch-visualstudio-test.cpp b/libs/libbsarch/examples/libbsarch-visualstudio-test.cpp new file mode 100644 index 0000000..1183ee9 --- /dev/null +++ b/libs/libbsarch/examples/libbsarch-visualstudio-test.cpp @@ -0,0 +1,190 @@ +#include +#include + +#include + +// #pragma comment(lib, "libbsarch") + +static const std::wstring separators(L"\\/"); + +std::wstring dirname(const std::wstring &path) +{ + size_t slash_pos = path.find_last_of(separators); + return path.substr(0, slash_pos); +} + +std::wstring basename(const std::wstring &path) +{ + size_t slash_pos = path.find_last_of(separators); + return path.substr(slash_pos + 1); +} + +bool mkdirp(const std::wstring &directory, bool basedir = true) +{ + DWORD attributes = ::GetFileAttributesW(directory.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) + { + std::size_t slash_pos = directory.find_last_of(separators); + if (slash_pos != std::wstring::npos) + { + mkdirp(directory.substr(0, slash_pos)); + } + return ::CreateDirectoryW(directory.c_str(), nullptr); + } + else + { + return true; + } +} + +void dds_info(bsa_archive_t archive, const wchar_t *file_path, bsa_dds_info_t *dds_info, void *context) +{ + // Mock result + dds_info->width = 2048; + dds_info->height = 2048; + dds_info->mipmaps = 12; + + printf("ba2 dds_info callback file: %ls, context: %s\n", file_path, (char *) context); +} + +int main() +{ + bsa_result_message_t result = {0}; + bsa_archive_t archive = bsa_create(); + + { + result = bsa_load_from_file(archive, L"test_read.bsa"); + if (result.code < 0) + printf("ba1 %ls\n", result.text); + + bsa_entry_list_t entries = bsa_entry_list_create(); + bsa_get_resource_list(archive, entries, L""); + + for (size_t index = 0; index < bsa_entry_list_count(entries); index++) + { + wchar_t filename[2048]; + wchar_t dest_filename[2048] = L"test_ba1\\"; + + bsa_entry_list_get(entries, index, 2048, filename); + wcscat_s(dest_filename, 2048, filename); + + printf("ba1 file: %ls %ls\n", filename, dest_filename); + + if (mkdirp(dirname(dest_filename))) + { + result = bsa_extract_file(archive, filename, dest_filename); + if (result.code < 0) + printf("ba1 %ls\n", result.text); + } + else + { + printf("ba1 could not create: %ls\n", dirname(dest_filename).c_str()); + } + } + + bsa_entry_list_free(entries); + + bsa_close(archive); + } + + { + bsa_entry_list_t entries = bsa_entry_list_create(); + bsa_entry_list_add(entries, L"textures\\grass\\test.dds"); + bsa_entry_list_add(entries, L"textures\\grass\\test2.dds"); + + bsa_create_archive(archive, L"test_write.bsa", baSSE, entries); + bsa_add_file_from_disk_root(archive, L"test_ba1\\", L"test_ba1\\textures\\grass\\test.dds"); + result = bsa_add_file_from_disk(archive, L"textures\\grass\\test2.dds", L"test_ba1\\textures\\grass\\test.dds"); + if (result.code < 0) + printf("ba1 %ls\n", result.text); + bsa_save(archive); + bsa_close(archive); + + bsa_entry_list_free(entries); + } + + bsa_file_dds_info_callback_set(archive, dds_info, (void *) "I was shared"); + + { + result = bsa_load_from_file(archive, L"test_read.ba2"); + if (result.code < 0) + printf("ba2 %ls\n", result.text); + + bsa_entry_list_t entries = bsa_entry_list_create(); + bsa_get_resource_list(archive, entries, L""); + + for (size_t index = 0; index < bsa_entry_list_count(entries); index++) + { + wchar_t filename[2048]; + wchar_t dest_filename[2048] = L"test_ba2\\"; + + bsa_entry_list_get(entries, index, 2048, filename); + wcscat_s(dest_filename, 2048, filename); + + printf("ba2 file: %ls %ls\n", filename, dest_filename); + + if (mkdirp(dirname(dest_filename))) + { + result = bsa_extract_file(archive, filename, dest_filename); + if (result.code < 0) + printf("ba2 %ls\n", result.text); + } + else + { + printf("ba2 could not create: %ls\n", dirname(dest_filename).c_str()); + } + } + + bsa_entry_list_free(entries); + + bsa_close(archive); + } + + { + bsa_entry_list_t entries = bsa_entry_list_create(); + bsa_entry_list_add(entries, L"textures\\grass\\test.dds"); + bsa_entry_list_add(entries, L"textures\\grass\\test2.dds"); + + bsa_create_archive(archive, L"test_write.ba2", baFO4dds, entries); + bsa_add_file_from_disk_root(archive, L"test_ba2\\", L"test_ba2\\textures\\grass\\test.dds"); + result = bsa_add_file_from_disk(archive, L"textures\\grass\\test2.dds", L"test_ba2\\textures\\grass\\test.dds"); + if (result.code < 0) + printf("ba2 %ls\n", result.text); + bsa_save(archive); + bsa_close(archive); + + bsa_entry_list_free(entries); + } + + /* + { + result = bsa_load_from_file(archive, L"test_large.ba2"); + if (result.code < 0) + printf("ba2 large %ls\n", result.text); + + bsa_entry_list_t entries = bsa_entry_list_create(); + bsa_get_resource_list(archive, entries, L""); + + for (size_t index = 0; index < bsa_entry_list_count(entries); index++) { + wchar_t filename[2048]; + wchar_t dest_filename[2048] = L"test_ba2_large\\"; + + bsa_entry_list_get(entries, index, 2048, filename); + wcscat_s(dest_filename, 2048, filename); + + printf("ba2 file large: %ls %ls\n", filename, dest_filename); + + if (mkdirp(dirname(dest_filename))) { + result = bsa_extract_file(archive, filename, dest_filename); + if (result.code < 0) + printf("ba2 large %ls\n", result.text); + } + else { + printf("ba2 large could not create: %ls\n", dirname(dest_filename).c_str()); + } + } + } + */ + + bsa_free(archive); +} diff --git a/libs/libbsarch/examples/libbsarch-visualstudio-test.sln b/libs/libbsarch/examples/libbsarch-visualstudio-test.sln new file mode 100644 index 0000000..8f105e4 --- /dev/null +++ b/libs/libbsarch/examples/libbsarch-visualstudio-test.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.156 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libbsarch-visualstudio-test", "libbsarch-visualstudio-test.vcxproj", "{8F73F848-939D-4862-B58B-26572CF20A5F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8F73F848-939D-4862-B58B-26572CF20A5F}.Debug|x64.ActiveCfg = Debug|x64 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Debug|x64.Build.0 = Debug|x64 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Debug|x86.ActiveCfg = Debug|Win32 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Debug|x86.Build.0 = Debug|Win32 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Release|x64.ActiveCfg = Release|x64 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Release|x64.Build.0 = Release|x64 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Release|x86.ActiveCfg = Release|Win32 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F05F0FBF-790B-4443-B2EC-E7033074D9E0} + EndGlobalSection +EndGlobal diff --git a/libs/libbsarch/examples/test_read.ba2 b/libs/libbsarch/examples/test_read.ba2 new file mode 100644 index 0000000..16162ca Binary files /dev/null and b/libs/libbsarch/examples/test_read.ba2 differ diff --git a/libs/libbsarch/examples/test_read.bsa b/libs/libbsarch/examples/test_read.bsa new file mode 100644 index 0000000..e493003 Binary files /dev/null and b/libs/libbsarch/examples/test_read.bsa differ diff --git a/libs/libbsarch/include/libbsarch/base_types.hpp b/libs/libbsarch/include/libbsarch/base_types.hpp new file mode 100644 index 0000000..2baf4be --- /dev/null +++ b/libs/libbsarch/include/libbsarch/base_types.hpp @@ -0,0 +1,66 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#pragma once + +#include "libbsarch.h" +#include "utils/convertible_string.hpp" +#include + +namespace libbsarch { +struct memory_blob : public bsa_result_buffer_t +{ + memory_blob(uint32_t buffer_size, bsa_buffer_t buffer_data, bsa_archive_t parent) + : bsa_result_buffer_t{buffer_size, buffer_data} + , parent_(parent) + {} + + memory_blob(bsa_result_buffer_t buffer, bsa_archive_t parent) + : bsa_result_buffer_t(buffer) + , parent_(parent) + {} + + bsa_archive_t parent_; + + memory_blob(memory_blob const &) = default; + ~memory_blob() { bsa_file_data_free(parent_, *this); } +}; + +struct disk_blob +{ + disk_blob(const convertible_string &root_dir, const convertible_string &absolute_path) + : path_in_archive(absolute_path) + , path_on_disk(absolute_path) + { + path_in_archive.remove_substring(root_dir); + } + + disk_blob(const convertible_string &path_in_archive, + const convertible_string &absolute_path, + [[maybe_unused]] bool decoy_parameter) + : path_in_archive(path_in_archive) + , path_on_disk(absolute_path) + {} + + convertible_string path_in_archive; + convertible_string path_on_disk; +}; + +struct bs_packed_file +{ + bs_packed_file(const disk_blob &blob) + : path_in_archive(blob.path_in_archive) + , data(blob.path_on_disk) + {} + + bs_packed_file(convertible_string path_in_archive, memory_blob blob) + : path_in_archive(std::move(path_in_archive)) + , data(blob) + {} + + convertible_string path_in_archive; + std::variant data; +}; + +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/bs_archive.h b/libs/libbsarch/include/libbsarch/bs_archive.h new file mode 100644 index 0000000..1954e99 --- /dev/null +++ b/libs/libbsarch/include/libbsarch/bs_archive.h @@ -0,0 +1,100 @@ +#pragma once + +#include "bs_archive_entries.h" +#include "libbsarch.hpp" + +namespace libbsarch { +class bs_archive +{ +public: + bs_archive(); + bs_archive(const bsa_archive_type_t &type); + virtual ~bs_archive(); + + /* Properties */ + convertible_string get_filename(); + bsa_archive_type_t get_type(); + uint32_t get_version(); + convertible_string get_format_name(); + uint32_t get_file_count(); + + uint32_t get_archive_flags(); + void set_archive_flags(uint32_t flags); + + uint32_t get_file_flags(); + void set_file_flags(uint32_t flags); + + void set_compressed(bool value); + bool get_compressed(); + + void set_share_data(bool value); + bool get_share_data(); + + /* Input-Output */ + virtual void load_from_disk(const convertible_string &archive_path); + virtual void create(const convertible_string &archiveName, const bs_archive_entries &entries); + virtual void create(const convertible_string &archiveName, + const bs_archive_entries &entries, + const bsa_archive_type_t type); + void save() const; + + /* Add to archive */ + virtual void add_file_from_disk(const disk_blob &blob); + virtual void add_file_from_memory(const convertible_string &path_in_archive, const memory_blob &memory_data); + + /* Extract */ + memory_blob extract_to_memory(bsa_file_record_t record); + memory_blob extract_to_memory(const convertible_string &filename); + void extract_to_disk(const convertible_string &filename, const convertible_string &save_as) const; + + /* Selectors */ + bsa_file_record_t find_file_record(const convertible_string &filename); + std::vector list_files() const; + + /* Other */ + + /*! \brief Sets the DDS callback. It is required to use Fallout 4 DDS archives. You do not have + * to manage the lifetime of the provided context + * \example + void dds_callback([[maybe_unused]] bsa_archive_t archive, + const wchar_t *file_path, + bsa_dds_info_t *dds_info, + void *context) + { + const auto &path = *static_cast(context) + '/' + convertible_string(file_path); + + auto image = std::make_unique(); + DirectX::TexMetadata info; + + const auto hr = LoadFromDDSFile(PREPARE_PATH_LIBBSARCH(path), DirectX::DDS_FLAGS_BAD_DXTN_TAILS, &info, *image); + if (FAILED(hr)) + { + //Do not throw here. Exceptions will be ignored + return; + } + + dds_info->width = info.width; + dds_info->height = info.height; + dds_info->mipmaps = info.mipLevels; + } + */ + template + void set_dds_callback(bsa_file_dds_info_proc_t dds_function, const context_type &context) + { + debug_log() << "Setting DDS callback for archive: " << archive_ << "\nCallback adress: " << &dds_function; + + auto heap_context = new context_type(context); + callback_contexts_.emplace_back(heap_context); + bsa_file_dds_info_callback_set(archive_, dds_function, callback_contexts_.back()); + } + + bsa_archive_t get_archive() const; + +protected: + bsa_archive_t archive_; + bsa_archive_type_t type_; + +private: + std::vector callback_contexts_; +}; +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/bs_archive_auto.hpp b/libs/libbsarch/include/libbsarch/bs_archive_auto.hpp new file mode 100644 index 0000000..c288f68 --- /dev/null +++ b/libs/libbsarch/include/libbsarch/bs_archive_auto.hpp @@ -0,0 +1,39 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#pragma once + +#include "bs_archive.h" + +namespace libbsarch { +/*! + * \brief The bs_archive_auto class is a convenience class. It manages both bs_archive and bs_archive_entries. + * It is recommended when packing from disk/extracting to disk + * Otherwise, if using memory, the use of these two separate classes is recommended + */ +class bs_archive_auto : public bs_archive +{ +protected: + bs_archive_entries entries_; + +public: + bs_archive_auto() = default; + bs_archive_auto(const bsa_archive_type_t &type); + + void add_file_from_disk(const disk_blob &blob) override; + void add_file_from_disk(const std::vector &blobs); + void add_file_from_memory(const convertible_string &path_in_archive, const memory_blob &memory_data) override; + void add_file(const bs_packed_file &file); + + void extract_all(const convertible_string &directory, bool overwrite_current_files); + + void load_from_disk(const convertible_string &archive_path) override; + void save_to_disk(const convertible_string &archive_path); + +private: + using bs_archive::create; //Use save_to_disk instead + using bs_archive::save; //Use save_to_disk instead + std::vector files_; +}; +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/bs_archive_entries.h b/libs/libbsarch/include/libbsarch/bs_archive_entries.h new file mode 100644 index 0000000..7df2f40 --- /dev/null +++ b/libs/libbsarch/include/libbsarch/bs_archive_entries.h @@ -0,0 +1,24 @@ +#pragma once + +#include "libbsarch.hpp" +#include + +namespace libbsarch { +class bs_archive_entries +{ +public: + bs_archive_entries(); + explicit bs_archive_entries(const std::vector &entries); + explicit bs_archive_entries(const bsa_entry_list_t &entries); + ~bs_archive_entries(); + + void add(const convertible_string &filepath); + uint32_t count(); + std::vector list(); + + bsa_entry_list_t getEntries() const; + +protected: + bsa_entry_list_t _entries; +}; +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/libbsarch.h b/libs/libbsarch/include/libbsarch/libbsarch.h new file mode 100644 index 0000000..bda7ca0 --- /dev/null +++ b/libs/libbsarch/include/libbsarch/libbsarch.h @@ -0,0 +1,157 @@ +#pragma once + +#include + +#include + +#include + +#ifdef _WIN32 + #ifdef BSARCH_DLL_EXPORT + #define BSARCH_DLL_API(ReturnType) extern "C" __declspec(dllexport) ReturnType __stdcall + #else + #define BSARCH_DLL_API(ReturnType) extern "C" __declspec(dllimport) ReturnType __stdcall + #endif +#else + #ifdef BSARCH_DLL_EXPORT + #define BSARCH_DLL_API(ReturnType) extern "C" __attribute__((visibility("default"))) ReturnType + #else + #define BSARCH_DLL_API(ReturnType) extern "C" ReturnType + #endif +#endif + +#ifdef __GNUC__ +#define PACKED(datastructure) datastructure __attribute__((__packed__)) +#else +#define PACKED(datastructure) __pragma(pack(push, 1)) datastructure __pragma(pack(pop)) +#endif + +typedef enum bsa_result_code_e +{ + BSA_RESULT_NONE = 0, + BSA_RESULT_EXCEPTION = -1 +} bsa_result_code_t; + +typedef void *bsa_buffer_t; +typedef void *bsa_archive_t; +typedef void *bsa_entry_list_t; +typedef void *bsa_file_record_t; +typedef void *bsa_folder_record_t; + +typedef struct bsa_dds_info_s +{ + uint32_t width, height, mipmaps; +} bsa_dds_info_t; + +PACKED(struct bsa_dds_header_s { + uint32_t magic; // DDS_MAGIC + DirectX::DDS_HEADER header; +}); + +typedef struct bsa_dds_header_s bsa_dds_header_t; + +PACKED(struct bsa_result_message_s { + int8_t code; // bsa_result_code_t + wchar_t text[1024]; +}); + +typedef struct bsa_result_message_s bsa_result_message_t; + +PACKED(struct bsa_result_buffer_s { + uint32_t size; + bsa_buffer_t data; +}); + +typedef struct bsa_result_buffer_s bsa_result_buffer_t; + +PACKED(struct bsa_result_message_buffer_s { + bsa_result_buffer_t buffer; + bsa_result_message_t message; +}); + +typedef struct bsa_result_message_buffer_s bsa_result_message_buffer_t; + +typedef enum bsa_archive_state_e +{ + stReading, + stWriting +} bsa_archive_state_t; + +typedef enum bsa_archive_type_e +{ + baNone, + baTES3, + baTES4, + baFO3, + baSSE, + baFO4, + baFO4dds, + baSF, + baSFdds +} bsa_archive_type_t; + +typedef void (*bsa_file_dds_info_proc_t)(bsa_archive_t archive, + const wchar_t *file_path, + bsa_dds_info_t *dds_info, + void *context); +typedef bool (*bsa_file_iteration_proc_t)(bsa_archive_t archive, + const wchar_t *file_path, + bsa_file_record_t file_record, + bsa_folder_record_t folder_record, + void *context); + +BSARCH_DLL_API(bsa_entry_list_t) bsa_entry_list_create(); +BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_free(bsa_entry_list_t entry_list); +BSARCH_DLL_API(uint32_t) bsa_entry_list_count(bsa_entry_list_t entry_list); +BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_add(bsa_entry_list_t entry_list, const wchar_t *entry_string); +BSARCH_DLL_API(uint32_t) +bsa_entry_list_get(bsa_entry_list_t entry_list, uint32_t index, uint32_t string_buffer_size, wchar_t *string_buffer); + +BSARCH_DLL_API(bsa_archive_t) bsa_create(); +BSARCH_DLL_API(bsa_result_message_t) bsa_free(bsa_archive_t archive); +BSARCH_DLL_API(bsa_result_message_t) bsa_load_from_file(bsa_archive_t archive, const wchar_t *file_path); +BSARCH_DLL_API(bsa_result_message_t) +bsa_create_archive(bsa_archive_t archive, + const wchar_t *file_path, + bsa_archive_type_t _archivetype, + bsa_entry_list_t entry_list); +BSARCH_DLL_API(bsa_result_message_t) bsa_save(bsa_archive_t archive); +BSARCH_DLL_API(bsa_result_message_t) +bsa_add_file_from_disk(bsa_archive_t archive, const wchar_t *file_path, const wchar_t *source_path); +BSARCH_DLL_API(bsa_result_message_t) +bsa_add_file_from_disk_root(bsa_archive_t archive, const wchar_t *root_dir, const wchar_t *source_path); +BSARCH_DLL_API(bsa_result_message_t) +bsa_add_file_from_memory(bsa_archive_t archive, const wchar_t *file_path, uint32_t size, bsa_buffer_t data); +BSARCH_DLL_API(bsa_file_record_t) bsa_find_file_record(bsa_archive_t archive, const wchar_t *file_path); +BSARCH_DLL_API(bsa_result_message_buffer_t) +bsa_extract_file_data_by_record(bsa_archive_t archive, bsa_file_record_t file_record); +BSARCH_DLL_API(bsa_result_message_buffer_t) +bsa_extract_file_data_by_filename(bsa_archive_t archive, const wchar_t *file_path); +BSARCH_DLL_API(bsa_result_message_t) bsa_file_data_free(bsa_archive_t archive, bsa_result_buffer_t file_data_result); +BSARCH_DLL_API(bsa_result_message_t) +bsa_extract_file(bsa_archive_t archive, const wchar_t *file_path, const wchar_t *save_as); +BSARCH_DLL_API(bsa_result_message_t) +bsa_iterate_files(bsa_archive_t archive, bsa_file_iteration_proc_t file_iteration_proc, void *context); +BSARCH_DLL_API(bool) bsa_file_exists(bsa_archive_t archive, const wchar_t *file_path); +BSARCH_DLL_API(bsa_result_message_t) +bsa_get_resource_list(bsa_archive_t archive, bsa_entry_list_t entry_result_list, const wchar_t *folder); +BSARCH_DLL_API(bsa_result_message_t) bsa_close(bsa_archive_t archive); + +BSARCH_DLL_API(uint32_t) +bsa_filename_get(bsa_archive_t archive, uint32_t string_buffer_size, wchar_t *string_buffer); +BSARCH_DLL_API(bsa_archive_type_t) bsa_archive_type_get(bsa_archive_t archive); +BSARCH_DLL_API(uint32_t) bsa_version_get(bsa_archive_t archive); +BSARCH_DLL_API(uint32_t) +bsa_format_name_get(bsa_archive_t archive, uint32_t string_buffer_size, wchar_t *string_buffer); +BSARCH_DLL_API(uint32_t) bsa_file_count_get(bsa_archive_t archive); +BSARCH_DLL_API(uint32_t) bsa_archive_flags_get(bsa_archive_t archive); +BSARCH_DLL_API(void) bsa_archive_flags_set(bsa_archive_t archive, uint32_t flags); +BSARCH_DLL_API(uint32_t) bsa_file_flags_get(bsa_archive_t archive); +BSARCH_DLL_API(void) bsa_file_flags_set(bsa_archive_t archive, uint32_t flags); +BSARCH_DLL_API(bool) bsa_compress_get(bsa_archive_t archive); +BSARCH_DLL_API(void) bsa_compress_set(bsa_archive_t archive, bool flags); +BSARCH_DLL_API(bool) bsa_share_data_get(bsa_archive_t archive); +BSARCH_DLL_API(void) bsa_share_data_set(bsa_archive_t archive, bool flags); + +BSARCH_DLL_API(void) +bsa_file_dds_info_callback_set(bsa_archive_t archive, bsa_file_dds_info_proc_t file_dds_info_proc, void *context); diff --git a/libs/libbsarch/include/libbsarch/libbsarch.hpp b/libs/libbsarch/include/libbsarch/libbsarch.hpp new file mode 100644 index 0000000..2a6acce --- /dev/null +++ b/libs/libbsarch/include/libbsarch/libbsarch.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "base_types.hpp" +#include "utils/convertible_ostream.hpp" +#include "utils/convertible_string.hpp" + +#include + +namespace libbsarch { +constexpr bool enable_debug_log = true; +[[maybe_unused]] constexpr int max_string_buffer_size = 1024; + +inline convertible_ostream &debug_log() +{ + static convertible_ostream ostr; + if constexpr (enable_debug_log) + return ostr << "[libbsarch] " << __FUNCTION__ << ' '; +} + +inline void checkResult(const bsa_result_message_s &result) +{ + if (result.code == BSA_RESULT_EXCEPTION) + { + // Copy from packed struct to aligned local buffer to avoid UB + wchar_t aligned_text[1024]; + std::memcpy(aligned_text, result.text, sizeof(aligned_text)); + const std::string error = to_string(std::wstring(aligned_text)); + debug_log() << error; + throw std::runtime_error(error); + } +} + +inline void checkResult(const bsa_result_message_buffer_s &result) +{ + checkResult(result.message); +} +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/utils/convertible_ostream.hpp b/libs/libbsarch/include/libbsarch/utils/convertible_ostream.hpp new file mode 100644 index 0000000..80babb0 --- /dev/null +++ b/libs/libbsarch/include/libbsarch/utils/convertible_ostream.hpp @@ -0,0 +1,37 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#pragma once + +#include "convertible_string.hpp" +#include + +namespace libbsarch { +class convertible_ostream +{ +public: + convertible_ostream &operator<<(const std::string &rh); + convertible_ostream &operator<<(const char *rh); + convertible_ostream &operator<<(const char rh); + convertible_ostream &operator<<(const std::wstring &rh); + convertible_ostream &operator<<(const void *rh); + convertible_ostream &operator<<(const convertible_string &rh); + + template::value, number>::type> + convertible_ostream &operator<<(const number &num) + { + std::cout << num; + return *this; + } + + operator std::ostream &(); + operator std::wostream &(); + +#ifdef LIBBSARCH_QT_SUPPORT + operator QDebug(); + convertible_ostream &operator<<(const QString &rh); +#endif +}; +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/utils/convertible_string.hpp b/libs/libbsarch/include/libbsarch/utils/convertible_string.hpp new file mode 100644 index 0000000..e6b996d --- /dev/null +++ b/libs/libbsarch/include/libbsarch/utils/convertible_string.hpp @@ -0,0 +1,50 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#pragma once + +//See https://stackoverflow.com/questions/4804298/how-to-convert-wstring-into-string + +#ifdef LIBBSARCH_QT_SUPPORT +#include +#endif + +#include "string_convert.hpp" + +namespace libbsarch { + +template +class convertible_string_converter; + +class convertible_string +{ +public: + // default ctor + convertible_string() = default; + + /* conversion ctors */ + convertible_string(std::string value, bool to_native_path = true); + convertible_string(const char *const val_array, bool to_native_path = true); + convertible_string(const std::wstring &wvalue, bool to_native_path = true); + convertible_string(const wchar_t *const wval_array, bool to_native_path = true); + + /* assignment operators */ + convertible_string &operator=(const std::string &value); + convertible_string &operator=(const std::wstring &wvalue); + convertible_string &operator=(const wchar_t *wvalue); + + /* implicit conversion operators */ + operator std::string() const; + operator std::wstring() const; + operator const wchar_t *() const; + + /* Util */ + bool remove_substring(const convertible_string &sub_str); + convertible_string &to_native_path(); + +private: + std::string str_; + bool auto_convert_to_native_path = true; +}; +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/utils/string_convert.hpp b/libs/libbsarch/include/libbsarch/utils/string_convert.hpp new file mode 100644 index 0000000..6086fe1 --- /dev/null +++ b/libs/libbsarch/include/libbsarch/utils/string_convert.hpp @@ -0,0 +1,18 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#pragma once + +#ifdef LIBBSARCH_QT_SUPPORT +#include +#endif + +#include +#include +#include + +namespace libbsarch { +std::string to_string(const std::wstring &str); +std::wstring to_wstring(const std::string &str); +} // namespace libbsarch diff --git a/libs/libbsarch/src/bs_archive.cpp b/libs/libbsarch/src/bs_archive.cpp new file mode 100644 index 0000000..b50ef5b --- /dev/null +++ b/libs/libbsarch/src/bs_archive.cpp @@ -0,0 +1,185 @@ +#include "bs_archive.h" + +namespace libbsarch { + +bs_archive::bs_archive() + : archive_(bsa_create()) +{} + +bs_archive::bs_archive(const bsa_archive_type_t &type) + : archive_(bsa_create()) + , type_(type) +{} + +bs_archive::~bs_archive() +{ + bsa_close(archive_); + bsa_free(archive_); +} + +/* Properties */ +convertible_string bs_archive::get_filename() +{ + wchar_t name[max_string_buffer_size]; + bsa_filename_get(archive_, max_string_buffer_size, name); + return name; +} + +bsa_archive_type_t bs_archive::get_type() +{ + return bsa_archive_type_get(archive_); +} + +uint32_t bs_archive::get_version() +{ + return bsa_version_get(archive_); +} + +convertible_string bs_archive::get_format_name() +{ + wchar_t format_name[max_string_buffer_size]; + bsa_format_name_get(archive_, max_string_buffer_size, format_name); + return format_name; +} + +uint32_t bs_archive::get_file_count() +{ + return bsa_file_count_get(archive_); +} + +uint32_t bs_archive::get_archive_flags() +{ + return bsa_archive_flags_get(archive_); +} + +void bs_archive::set_archive_flags(uint32_t flags) +{ + bsa_archive_flags_set(archive_, flags); +} + +uint32_t bs_archive::get_file_flags() +{ + return bsa_file_flags_get(archive_); +} + +void bs_archive::set_file_flags(uint32_t flags) +{ + bsa_file_flags_set(archive_, flags); +} + +void bs_archive::set_compressed(bool value) +{ + bsa_compress_set(archive_, value); +} + +void bs_archive::set_share_data(bool value) +{ + bsa_share_data_set(archive_, value); +} + +bool bs_archive::get_compressed() +{ + return bsa_compress_get(archive_); +} + +bool bs_archive::get_share_data() +{ + return bsa_share_data_get(archive_); +} + +void bs_archive::load_from_disk(const convertible_string &archive_path) +{ + debug_log() << "Opening archive: " << archive_path; + const std::wstring str = archive_path; + const auto &result = bsa_load_from_file(archive_, str.c_str()); + libbsarch::checkResult(result); +} + +void bs_archive::create(const convertible_string &archiveName, const bs_archive_entries &entries) +{ + create(archiveName, entries, type_); +} + +void bs_archive::create(const convertible_string &archiveName, + const bs_archive_entries &entries, + const bsa_archive_type_t type) +{ + debug_log() << "Creating archive. Archive name: " << archiveName << '\n' + << "type: " << type_ << '\n' + << "entries: " << entries.getEntries(); + bsa_create_archive(archive_, archiveName, type, entries.getEntries()); +} + +void bs_archive::save() const +{ + debug_log() << "Saving archive: " << archive_; + + const auto &result = bsa_save(archive_); + + libbsarch::checkResult(result); +} + +void bs_archive::add_file_from_disk(const disk_blob &blob) +{ + debug_log() << "Adding file from disk root. Path in archive: " << blob.path_in_archive << '\n' + << "Filepath: " << blob.path_on_disk << '\n' + << "Archive: " << archive_; + + const auto &result = bsa_add_file_from_disk(archive_, blob.path_in_archive, blob.path_on_disk); + + libbsarch::checkResult(result); +} + +void bs_archive::add_file_from_memory(const convertible_string &path_in_archive, + const memory_blob &memory_data) //NOTE UNTESTED +{ + debug_log() << "Adding file from memory. Filename: " << path_in_archive << '\n' + << "Data size: " << memory_data.size; + const auto &result = bsa_add_file_from_memory(archive_, path_in_archive, memory_data.size, memory_data.data); + libbsarch::checkResult(result); +} + +bsa_file_record_t bs_archive::find_file_record(const convertible_string &filename) +{ + debug_log() << "Finding file record of: " << filename; + const auto &result = bsa_find_file_record(archive_, filename); + return result; +} + +memory_blob bs_archive::extract_to_memory(bsa_file_record_t record) +{ + const auto &result = bsa_extract_file_data_by_record(archive_, record); + libbsarch::checkResult(result); + return memory_blob(result.buffer, archive_); +} + +memory_blob bs_archive::extract_to_memory(const convertible_string &filename) +{ + const auto &result = bsa_extract_file_data_by_filename(archive_, filename); + libbsarch::checkResult(result); + return memory_blob(result.buffer, archive_); +} + +void bs_archive::extract_to_disk(const convertible_string &filename, const convertible_string &save_as) const +{ + debug_log() << "Extracting: " << filename << " saved as " << save_as; + const auto &result = bsa_extract_file(archive_, filename, save_as); + + libbsarch::checkResult(result); +} + +std::vector bs_archive::list_files() const +{ + bsa_entry_list_t list = bsa_entry_list_create(); + const auto &result = bsa_get_resource_list(archive_, list, L""); + + libbsarch::checkResult(result); + + return bs_archive_entries(list).list(); +} + +bsa_archive_t bs_archive::get_archive() const +{ + return archive_; +} +} // namespace libbsarch diff --git a/libs/libbsarch/src/bs_archive_auto.cpp b/libs/libbsarch/src/bs_archive_auto.cpp new file mode 100644 index 0000000..1ad8e3e --- /dev/null +++ b/libs/libbsarch/src/bs_archive_auto.cpp @@ -0,0 +1,87 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "bs_archive_auto.hpp" +#include +namespace libbsarch { +bs_archive_auto::bs_archive_auto(const bsa_archive_type_t &type) + : bs_archive(type) +{} + +void bs_archive_auto::add_file_from_disk(const disk_blob &blob) +{ + files_.emplace_back(blob); +} + +void bs_archive_auto::add_file_from_disk(const std::vector &blobs) +{ + files_.reserve(blobs.size()); + files_.insert(files_.end(), blobs.cbegin(), blobs.cend()); +} + +void bs_archive_auto::add_file_from_memory(const convertible_string &path_in_archive, const memory_blob &memory_data) +{ + files_.emplace_back(bs_packed_file(path_in_archive, memory_data)); +} + +void bs_archive_auto::add_file(const bs_packed_file &file) +{ + files_.emplace_back(file); +} + +void bs_archive_auto::extract_all(const convertible_string &directory, bool overwrite_current_files) +{ + namespace fs = std::filesystem; + for (const auto &file : list_files()) + { + convertible_string path_string = std::string(directory) + "/" + std::string(file); + fs::path current_file(path_string); + fs::path current_path = fs::path(current_file).remove_filename(); + + fs::create_directories(current_path); + if (fs::exists(current_file) && overwrite_current_files) + { + fs::remove(current_file); + extract_to_disk(file, current_file.string()); + } + else if (!fs::exists(current_file)) + extract_to_disk(file, current_file.string()); + } +} + +void bs_archive_auto::load_from_disk(const convertible_string &archive_path) +{ + bs_archive::load_from_disk(archive_path); + for (const auto &file : list_files()) + entries_.add(file); +} + +void bs_archive_auto::save_to_disk(const convertible_string &archive_path) +{ + for (const auto &file : files_) + entries_.add(file.path_in_archive); + + bs_archive::create(archive_path, entries_); + + for (const auto &file : files_) + { + switch (file.data.index()) + { + case 0: { + const auto &blob = std::get(file.data); + bs_archive::add_file_from_memory(file.path_in_archive, blob); + break; + } + case 1: { + const auto &file_path = std::get(file.data); + const auto &blob = disk_blob(file.path_in_archive, file_path, bool()); + bs_archive::add_file_from_disk(blob); + } + } + } + bs_archive::save(); +} + +} // namespace libbsarch diff --git a/libs/libbsarch/src/bs_archive_entries.cpp b/libs/libbsarch/src/bs_archive_entries.cpp new file mode 100644 index 0000000..4dc9c6d --- /dev/null +++ b/libs/libbsarch/src/bs_archive_entries.cpp @@ -0,0 +1,52 @@ +#include "bs_archive_entries.h" + +namespace libbsarch { +bs_archive_entries::bs_archive_entries() + : _entries(bsa_entry_list_create()) +{} + +bs_archive_entries::bs_archive_entries(const std::vector &entries) + : _entries(bsa_entry_list_create()) +{ + for (const auto &entry : entries) + add(entry); +} + +bs_archive_entries::bs_archive_entries(const bsa_entry_list_t &entries) + : _entries(entries) +{} + +bs_archive_entries::~bs_archive_entries() +{ + bsa_entry_list_free(_entries); +} + +void bs_archive_entries::add(const convertible_string &filepath) +{ + debug_log() << "Adding to entries: " << filepath; + const auto &result = bsa_entry_list_add(_entries, filepath); + libbsarch::checkResult(result); +} + +uint32_t bs_archive_entries::count() +{ + return bsa_entry_list_count(_entries); +} + +std::vector bs_archive_entries::list() +{ + std::vector list; + for (uint32_t i = 0; i < count(); ++i) + { + wchar_t buffer[max_string_buffer_size]; + bsa_entry_list_get(_entries, i, max_string_buffer_size, buffer); + list.push_back(buffer); + } + return list; +} + +bsa_entry_list_t bs_archive_entries::getEntries() const +{ + return _entries; +} +} // namespace libbsarch diff --git a/libs/libbsarch/src/libbsarch.cpp b/libs/libbsarch/src/libbsarch.cpp new file mode 100644 index 0000000..52ded9f --- /dev/null +++ b/libs/libbsarch/src/libbsarch.cpp @@ -0,0 +1,206 @@ +#ifdef BSARCH_DLL_EXPORT +#include "libbsarch.h" + +namespace libbsarch { + +BSARCH_DLL_API(bsa_entry_list_t) bsa_entry_list_create() +{ + return nullptr; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_free(bsa_entry_list_t entry_list) +{ + return {0}; +} + +BSARCH_DLL_API(uint32_t) bsa_entry_list_count(bsa_entry_list_t entry_list) +{ + return 0; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_add(bsa_entry_list_t entry_list, const wchar_t *entry_string) +{ + return {0}; +} + +BSARCH_DLL_API(uint32_t) +bsa_entry_list_get(bsa_entry_list_t entry_list, uint32_t index, uint32_t string_buffer_size, wchar_t *string_buffer) +{ + return 0; +} + +BSARCH_DLL_API(bsa_archive_t) bsa_create() +{ + return nullptr; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_free(bsa_archive_t archive) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_load_from_file(bsa_archive_t archive, const wchar_t *file_path) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_create_archive(bsa_archive_t archive, + const wchar_t *file_path, + bsa_archive_type_t archive_type, + bsa_entry_list_t entry_list) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_save(bsa_archive_t archive) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_add_file_from_disk(bsa_archive_t archive, const wchar_t *file_path, const wchar_t *source_path) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_add_file_from_disk_root(bsa_archive_t archive, const wchar_t *root_dir, const wchar_t *source_path) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_add_file_from_memory(bsa_archive_t archive, const wchar_t *file_path, uint32_t size, bsa_buffer_t data) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_file_record_t) bsa_find_file_record(bsa_archive_t archive, const wchar_t *file_path) +{ + return nullptr; +} + +BSARCH_DLL_API(bsa_result_message_buffer_t) +bsa_extract_file_data_by_record(bsa_archive_t archive, bsa_file_record_t file_record) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_buffer_t) +bsa_extract_file_data_by_filename(bsa_archive_t archive, const wchar_t *file_path) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_file_data_free(bsa_archive_t archive, bsa_result_buffer_t file_data_result) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_extract_file(bsa_archive_t archive, const wchar_t *file_path, const wchar_t *save_as) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_iterate_files(bsa_archive_t archive, bsa_file_iteration_proc_t file_iteration_proc, void *context) +{ + return {0}; +} + +BSARCH_DLL_API(bool) bsa_file_exists(bsa_archive_t archive, const wchar_t *file_path) +{ + return false; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_get_resource_list(bsa_archive_t archive, bsa_entry_list_t entry_result_list, const wchar_t *folder) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_resolve_hash(bsa_archive_t archive, uint64_t hash, bsa_entry_list_t entry_result_list) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_close(bsa_archive_t archive) +{ + return {0}; +} + +BSARCH_DLL_API(uint32_t) bsa_filename_get(bsa_archive_t archive, uint32_t string_buffer_size, wchar_t *string_buffer) +{ + return 0; +} + +BSARCH_DLL_API(bsa_archive_type_t) bsa_archive_type_get(bsa_archive_t archive) +{ + return bsa_archive_type_t::baNone; +} + +BSARCH_DLL_API(uint32_t) bsa_version_get(bsa_archive_t archive) +{ + return 0; +} + +BSARCH_DLL_API(uint32_t) bsa_format_name_get(bsa_archive_t archive, uint32_t string_buffer_size, wchar_t *string_buffer) +{ + return 0; +} + +BSARCH_DLL_API(uint32_t) bsa_file_count_get(bsa_archive_t archive) +{ + return 0; +} + +BSARCH_DLL_API(uint32_t) bsa_archive_flags_get(bsa_archive_t archive) +{ + return 0; +} + +BSARCH_DLL_API(void) bsa_archive_flags_set(bsa_archive_t archive, uint32_t flags) +{ + return; +} + +BSARCH_DLL_API(uint32_t) bsa_file_flags_get(bsa_archive_t archive) +{ + return 0; +} + +BSARCH_DLL_API(void) bsa_file_flags_set(bsa_archive_t archive, uint32_t flags) +{ + return; +} + +BSARCH_DLL_API(bool) bsa_compress_get(bsa_archive_t archive) +{ + return false; +} + +BSARCH_DLL_API(void) bsa_compress_set(bsa_archive_t archive, bool flags) +{ + return; +} + +BSARCH_DLL_API(bool) bsa_share_data_get(bsa_archive_t archive) +{ + return false; +} + +BSARCH_DLL_API(void) bsa_share_data_set(bsa_archive_t archive, bool flags) +{ + return; +} + +BSARCH_DLL_API(void) +bsa_file_dds_info_callback_set(bsa_archive_t archive, bsa_file_dds_info_proc_t file_dds_info_proc, void *context) +{ + return; +} +} // namespace libbsarch +#endif diff --git a/libs/libbsarch/src/libbsarch.def b/libs/libbsarch/src/libbsarch.def new file mode 100644 index 0000000..d6386b0 --- /dev/null +++ b/libs/libbsarch/src/libbsarch.def @@ -0,0 +1,38 @@ +;DESCRIPTION "BSArch DLL" +EXPORTS + bsa_entry_list_create + bsa_entry_list_free + bsa_entry_list_count + bsa_entry_list_add + bsa_entry_list_get + bsa_create + bsa_free + bsa_load_from_file + bsa_create_archive + bsa_save + bsa_add_file_from_disk + bsa_add_file_from_disk_root + bsa_add_file_from_memory + bsa_find_file_record + bsa_extract_file_data_by_record + bsa_extract_file_data_by_filename + bsa_file_data_free + bsa_extract_file + bsa_iterate_files + bsa_file_exists + bsa_get_resource_list + bsa_close + bsa_filename_get + bsa_archive_type_get + bsa_version_get + bsa_format_name_get + bsa_file_count_get + bsa_archive_flags_get + bsa_archive_flags_set + bsa_file_flags_get + bsa_file_flags_set + bsa_compress_get + bsa_compress_set + bsa_share_data_get + bsa_share_data_set + bsa_file_dds_info_callback_set diff --git a/libs/libbsarch/src/utils/convertible_ostream.cpp b/libs/libbsarch/src/utils/convertible_ostream.cpp new file mode 100644 index 0000000..d5ac238 --- /dev/null +++ b/libs/libbsarch/src/utils/convertible_ostream.cpp @@ -0,0 +1,58 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#include "convertible_ostream.hpp" + +namespace libbsarch { +convertible_ostream &convertible_ostream::operator<<(const std::string &rh) +{ + std::cout << rh; + return *this; +} +convertible_ostream &convertible_ostream::operator<<(const char *rh) +{ + std::cout << rh; + return *this; +} +convertible_ostream &convertible_ostream::operator<<(const char rh) +{ + std::cout << rh; + return *this; +} +convertible_ostream &convertible_ostream::operator<<(const std::wstring &rh) +{ + std::wcout << rh; + return *this; +} +convertible_ostream &convertible_ostream::operator<<(const void *rh) +{ + std::cout << rh; + return *this; +} +convertible_ostream &convertible_ostream::operator<<(const convertible_string &rh) +{ + return *this << std::string(rh); +} + +convertible_ostream::operator std::ostream &() +{ + return std::cout; +} +convertible_ostream::operator std::wostream &() +{ + return std::wcout; +} + +#ifdef LIBBSARCH_QT_SUPPORT +convertible_ostream::operator QDebug() +{ + return qDebug(); +} +convertible_ostream &convertible_ostream::operator<<(const QString &rh) +{ + qDebug() << rh; + return *this; +} +#endif +} // namespace libbsarch diff --git a/libs/libbsarch/src/utils/convertible_string.cpp b/libs/libbsarch/src/utils/convertible_string.cpp new file mode 100644 index 0000000..c965202 --- /dev/null +++ b/libs/libbsarch/src/utils/convertible_string.cpp @@ -0,0 +1,108 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "convertible_string.hpp" +#include +#include + +namespace libbsarch { +/* conversion ctors */ +convertible_string::convertible_string(std::string value, bool to_native_path) + : str_(std::move(value)) + , auto_convert_to_native_path(to_native_path) +{ + if (auto_convert_to_native_path) + this->to_native_path(); +} + +convertible_string::convertible_string(const char *const val_array, bool to_native_path) + : str_(std::string(val_array)) + , auto_convert_to_native_path(to_native_path) + +{ + if (auto_convert_to_native_path) + this->to_native_path(); +} +convertible_string::convertible_string(const std::wstring &wvalue, bool to_native_path) + : str_(to_string(wvalue)) + , auto_convert_to_native_path(to_native_path) + +{ + if (auto_convert_to_native_path) + this->to_native_path(); +} +convertible_string::convertible_string(wchar_t const *wval_array, bool to_native_path) + : str_(to_string(std::wstring(wval_array))) + , auto_convert_to_native_path(to_native_path) +{ + if (auto_convert_to_native_path) + this->to_native_path(); +} + +/* assignment operators */ +convertible_string &convertible_string::operator=(const std::string &value) +{ + str_ = value; + if (auto_convert_to_native_path) + this->to_native_path(); + return *this; +} +convertible_string &convertible_string::operator=(const std::wstring &wvalue) +{ + str_ = to_string(wvalue); + if (auto_convert_to_native_path) + this->to_native_path(); + return *this; +} + +convertible_string &convertible_string::operator=(const wchar_t *wvalue) +{ + str_ = to_string(std::wstring(wvalue)); + if (auto_convert_to_native_path) + this->to_native_path(); + return *this; +} + +/* implicit conversion operators */ +convertible_string::operator std::string() const +{ + return str_; +} +convertible_string::operator std::wstring() const +{ + return to_wstring(str_); +} +convertible_string::operator const wchar_t *() const +{ + const std::wstring wstr = to_wstring(str_); + wchar_t *wchar_array = new wchar_t[wstr.length() + 1]; +#ifdef _WIN32 + wcscpy_s(wchar_array, wstr.length() + 1, wstr.c_str()); +#else + std::wmemcpy(wchar_array, wstr.c_str(), wstr.length() + 1); +#endif + return wchar_array; +} + +/* Util */ +bool convertible_string::remove_substring(const convertible_string &sub_str) +{ + size_t pos = str_.find(sub_str); + if (pos != std::string::npos) + { + str_.erase(pos, std::string(sub_str).length()); + return true; + } + return false; +} + +convertible_string &convertible_string::to_native_path() +{ + std::filesystem::path path(str_); + str_ = path.make_preferred().string(); + return *this; +} + +} // namespace libbsarch diff --git a/libs/libbsarch/src/utils/string_convert.cpp b/libs/libbsarch/src/utils/string_convert.cpp new file mode 100644 index 0000000..1e8ce9e --- /dev/null +++ b/libs/libbsarch/src/utils/string_convert.cpp @@ -0,0 +1,39 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#include "string_convert.hpp" + +#include +#include + +namespace libbsarch { + +std::string to_string(const std::wstring &str) +{ + if (str.empty()) return {}; + std::mbstate_t state{}; + const wchar_t* src = str.data(); + std::size_t len = std::wcsrtombs(nullptr, &src, 0, &state); + if (len == static_cast(-1)) return {}; + std::string result(len, '\0'); + src = str.data(); + state = std::mbstate_t{}; + std::wcsrtombs(result.data(), &src, len + 1, &state); + return result; +} + +std::wstring to_wstring(const std::string &str) +{ + if (str.empty()) return {}; + std::mbstate_t state{}; + const char* src = str.data(); + std::size_t len = std::mbsrtowcs(nullptr, &src, 0, &state); + if (len == static_cast(-1)) return {}; + std::wstring result(len, L'\0'); + src = str.data(); + state = std::mbstate_t{}; + std::mbsrtowcs(result.data(), &src, len + 1, &state); + return result; +} +} // namespace libbsarch diff --git a/libs/libbsarch/vcpkg.json b/libs/libbsarch/vcpkg.json new file mode 100644 index 0000000..3cfe0a0 --- /dev/null +++ b/libs/libbsarch/vcpkg.json @@ -0,0 +1,18 @@ +{ + "dependencies": ["mo2-dds-header"], + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "f61a294e765b257926ae9e9d85f96468a0af74e7" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "69db61b22147b14ad66fd05cf798d855f6d68c12", + "packages": ["mo2-*"] + } + ] + } +} diff --git a/libs/lootcli/.clang-format b/libs/lootcli/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/lootcli/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/lootcli/.git-blame-ignore-revs b/libs/lootcli/.git-blame-ignore-revs new file mode 100644 index 0000000..28904f1 --- /dev/null +++ b/libs/lootcli/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +dfac8a52f654858beacb3f908234388e70fd88a2 +95a7b8b4226e401db8b7c1a13b1bbbbd8cb5c2cf diff --git a/libs/lootcli/.gitattributes b/libs/lootcli/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/lootcli/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/lootcli/.github/workflows/build.yml b/libs/lootcli/.github/workflows/build.yml new file mode 100644 index 0000000..d9ca97e --- /dev/null +++ b/libs/lootcli/.github/workflows/build.yml @@ -0,0 +1,18 @@ +name: Build LootCLI +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build LootCLI + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: cmake_common diff --git a/libs/lootcli/.github/workflows/linting.yml b/libs/lootcli/.github/workflows/linting.yml new file mode 100644 index 0000000..b457e70 --- /dev/null +++ b/libs/lootcli/.github/workflows/linting.yml @@ -0,0 +1,15 @@ +name: Lint LootCLI +on: + push: + pull_request: + types: [opened, synchronize, reopened] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Run clang-format + uses: jidicula/clang-format-action@v4.11.0 + with: + clang-format-version: "15" + check-path: "." diff --git a/libs/lootcli/.gitignore b/libs/lootcli/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/lootcli/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/lootcli/.pre-commit-config.yaml b/libs/lootcli/.pre-commit-config.yaml new file mode 100644 index 0000000..3103a1f --- /dev/null +++ b/libs/lootcli/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v19.1.5 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/lootcli/CMakeLists.txt b/libs/lootcli/CMakeLists.txt new file mode 100644 index 0000000..35e9d98 --- /dev/null +++ b/libs/lootcli/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.16) + +include(CMakePackageConfigHelpers) + +project(lootcli) + +add_subdirectory(src) + +# install the header helper +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/mo2-lootcli-header-config.cmake" + INSTALL_DESTINATION "lib/cmake/mo2-lootcli-header" + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO +) + +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/src/version.h" lootcli_version) +string(REGEX MATCH "#define LOOTCLI_VERSION_STRING[ \t]*\"([0-9.]+)\"" _ ${lootcli_version}) +set(lootcli_version ${CMAKE_MATCH_1}) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/mo2-lootcli-header-config-version.cmake" + VERSION "${lootcli_version}" + COMPATIBILITY AnyNewerVersion + ARCH_INDEPENDENT +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/mo2-lootcli-header-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/mo2-lootcli-header-config-version.cmake + DESTINATION lib/cmake/mo2-lootcli-header +) diff --git a/libs/lootcli/CMakePresets.json b/libs/lootcli/CMakePresets.json new file mode 100644 index 0000000..023baf5 --- /dev/null +++ b/libs/lootcli/CMakePresets.json @@ -0,0 +1,52 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg"], + "name": "vs2022-windows", + "toolset": "v143" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/lootcli/README.md b/libs/lootcli/README.md new file mode 100644 index 0000000..1dc0ded --- /dev/null +++ b/libs/lootcli/README.md @@ -0,0 +1,3 @@ +[![Build status](https://ci.appveyor.com/api/projects/status/e9k28w726ucmd0cu?svg=true)](https://ci.appveyor.com/project/Modorganizer2/modorganizer-lootcli) + +# modorganizer-lootcli diff --git a/libs/lootcli/cmake/config.cmake.in b/libs/lootcli/cmake/config.cmake.in new file mode 100644 index 0000000..4c845a2 --- /dev/null +++ b/libs/lootcli/cmake/config.cmake.in @@ -0,0 +1,3 @@ +@PACKAGE_INIT@ + +include ( "${CMAKE_CURRENT_LIST_DIR}/mo2-lootcli-header-targets.cmake" ) diff --git a/libs/lootcli/include/lootcli/lootcli.h b/libs/lootcli/include/lootcli/lootcli.h new file mode 100644 index 0000000..90305a6 --- /dev/null +++ b/libs/lootcli/include/lootcli/lootcli.h @@ -0,0 +1,116 @@ +#ifndef MODORGANIZER_LOOTCLI_INCLUDED +#define MODORGANIZER_LOOTCLI_INCLUDED + +#include + +namespace lootcli +{ + +enum class LogLevels +{ + Trace = 0, + Debug, + Info, + Warning, + Error +}; + +inline LogLevels logLevelFromString(const std::string& s) +{ + if (s == "trace") { + return LogLevels::Trace; + } else if (s == "debug") { + return LogLevels::Debug; + } else if (s == "info") { + return LogLevels::Info; + } else if (s == "warning") { + return LogLevels::Warning; + } else if (s == "error") { + return LogLevels::Error; + } else { + return LogLevels::Info; + } +} + +inline std::string logLevelToString(LogLevels level) +{ + switch (level) { + case LogLevels::Trace: + return "trace"; + case LogLevels::Debug: + return "debug"; + case LogLevels::Info: + return "info"; + case LogLevels::Warning: + return "warning"; + case LogLevels::Error: + return "error"; + default: + return "info"; + } +} + +enum class Progress +{ + None = 0, + CheckingMasterlistExistence, + UpdatingMasterlist, + LoadingLists, + ReadingPlugins, + SortingPlugins, + WritingLoadorder, + ParsingLootMessages, + Done +}; + +enum class MessageType +{ + None = 0, + Progress, + Log +}; + +struct Message +{ + MessageType type = MessageType::None; + Progress progress = Progress::None; + LogLevels logLevel = LogLevels::Info; + std::string log; + + static Message fromProgress(Progress p) + { + return {MessageType::Progress, p, LogLevels::Info, ""}; + } + + static Message fromLog(LogLevels level, std::string log) + { + return {MessageType::Log, Progress::None, level, std::move(log)}; + } +}; + +inline Message parseMessage(const std::string_view& line) +{ + static std::regex e(R"(^\[([a-z]+)\] (.+)$)"); + + std::match_results m; + if (!std::regex_match(line.begin(), line.end(), m, e)) { + return {}; + } + + const auto type = m[1]; + + if (type == "progress") { + try { + const auto p = std::stoi(m[2]); + return Message::fromProgress(static_cast(p)); + } catch (std::exception&) { + return {}; + } + } else { + return Message::fromLog(logLevelFromString(type), m[2]); + } +} + +} // namespace lootcli + +#endif // MODORGANIZER_LOOTCLI_INCLUDED diff --git a/libs/lootcli/src/CMakeLists.txt b/libs/lootcli/src/CMakeLists.txt new file mode 100644 index 0000000..fdc292e --- /dev/null +++ b/libs/lootcli/src/CMakeLists.txt @@ -0,0 +1,138 @@ +cmake_minimum_required(VERSION 3.16) + +# Qt is not required, this allows us to skip building the executable and only create +# the single-header interface when using this as a VCPKG dependency + +find_package(Qt6 CONFIG COMPONENTS Core) + +if (Qt6_FOUND) + +message(STATUS "Qt6 found, building lootcli executable") + +find_package(tomlplusplus CONFIG REQUIRED) +find_package(Boost REQUIRED CONFIG COMPONENTS locale) + +# Try to find libloot via cmake config, then fall back to pkg-config +find_package(libloot CONFIG QUIET) +if(NOT libloot_FOUND) + # On Linux with AUR package, libloot installs a .pc file + find_package(PkgConfig) + if(PkgConfig_FOUND) + pkg_check_modules(LIBLOOT REQUIRED IMPORTED_TARGET libloot) + endif() +endif() + +if(TARGET libloot::loot) + # avoid CMake error/warning + set_target_properties(libloot::loot PROPERTIES + MAP_IMPORTED_CONFIG_RELEASE RelWithDebInfo + MAP_IMPORTED_CONFIG_MINSIZEREL RelWithDebInfo + ) +endif() + +if(WIN32) + add_executable(lootcli WIN32) + set_target_properties(lootcli PROPERTIES + CXX_STANDARD 20 + WIN32_EXECUTABLE TRUE) +else() + add_executable(lootcli) + set_target_properties(lootcli PROPERTIES + CXX_STANDARD 20) +endif() + +set(LOOTCLI_SOURCES + game_settings.cpp + game_settings.h + lootthread.cpp + lootthread.h + main.cpp + pch.h + version.h + ${CMAKE_CURRENT_SOURCE_DIR}/../include/lootcli/lootcli.h +) + +if(WIN32) + list(APPEND LOOTCLI_SOURCES version.rc) +endif() + +target_sources(lootcli PRIVATE ${LOOTCLI_SOURCES}) + +if(WIN32) + target_compile_definitions(lootcli + PRIVATE + _UNICODE UNICODE + _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING) +endif() + +target_include_directories(lootcli PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include) +target_precompile_headers(lootcli PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/pch.h) + +# Link libraries +if(TARGET libloot::loot) + target_link_libraries(lootcli PRIVATE libloot::loot) +elseif(TARGET PkgConfig::LIBLOOT) + target_link_libraries(lootcli PRIVATE PkgConfig::LIBLOOT) +else() + # Fallback: try to find the library manually + find_library(LIBLOOT_LIBRARY NAMES loot libloot) + find_path(LIBLOOT_INCLUDE_DIR NAMES loot/api.h) + if(LIBLOOT_LIBRARY AND LIBLOOT_INCLUDE_DIR) + target_link_libraries(lootcli PRIVATE ${LIBLOOT_LIBRARY}) + target_include_directories(lootcli PRIVATE ${LIBLOOT_INCLUDE_DIR}) + else() + message(FATAL_ERROR "libloot not found. Install libloot or set CMAKE_PREFIX_PATH.") + endif() +endif() + +target_link_libraries(lootcli + PRIVATE Boost::headers Boost::locale + tomlplusplus::tomlplusplus Qt6::Core) + +if(NOT WIN32) + find_package(CURL REQUIRED) + target_link_libraries(lootcli PRIVATE CURL::libcurl) +endif() + +if (MSVC) + target_compile_options(lootcli + PRIVATE + "/MP" + "/W4" + "/external:anglebrackets" + "/external:W0" + ) + target_link_options(lootcli + PRIVATE + $<$:/LTCG /INCREMENTAL:NO /OPT:REF /OPT:ICF> + ) + target_compile_definitions(lootcli PRIVATE _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING) + + set_target_properties(lootcli PROPERTIES VS_STARTUP_PROJECT lootcli) +endif() + +if(WIN32) + install(FILES + $ + $ + DESTINATION bin/loot) +else() + install(TARGETS lootcli DESTINATION bin/loot) +endif() + +endif() + +# library to make the header available +add_library(lootcli-header INTERFACE) +target_sources(lootcli-header INTERFACE + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_LIST_DIR}/../include + FILES ${CMAKE_CURRENT_LIST_DIR}/../include/lootcli/lootcli.h) +add_library(mo2::lootcli-header ALIAS lootcli-header) + +install(TARGETS lootcli-header EXPORT lootcliHeaderTargets FILE_SET HEADERS) +install(EXPORT lootcliHeaderTargets + FILE mo2-lootcli-header-targets.cmake + NAMESPACE mo2:: + DESTINATION lib/cmake/mo2-lootcli-header +) diff --git a/libs/lootcli/src/game_settings.cpp b/libs/lootcli/src/game_settings.cpp new file mode 100644 index 0000000..00d74c0 --- /dev/null +++ b/libs/lootcli/src/game_settings.cpp @@ -0,0 +1,435 @@ +#include "game_settings.h" +#include + +namespace fs = std::filesystem; +namespace loot +{ +static constexpr float MORROWIND_MINIMUM_HEADER_VERSION = 1.2f; +static constexpr float OBLIVION_MINIMUM_HEADER_VERSION = 0.8f; +static constexpr float SKYRIM_FO3_MINIMUM_HEADER_VERSION = 0.94f; +static constexpr float SKYRIM_SE_MINIMUM_HEADER_VERSION = 1.7f; +static constexpr float FONV_MINIMUM_HEADER_VERSION = 1.32f; +static constexpr float FO4_MINIMUM_HEADER_VERSION = 0.95f; +static constexpr float STARFIELD_MINIMUM_HEADER_VERSION = 0.96f; + +std::filesystem::path GetOpenMWDataPath(const std::filesystem::path& gamePath) +{ +#ifndef _WIN32 + if (gamePath == "/usr/games") { + // Ubuntu, Debian + return "/usr/share/games/openmw/resources/vfs"; + } else if (gamePath == "/run/host/usr/games") { + // Ubuntu, Debian from inside a Flatpak sandbox + return "/run/host/usr/share/games/openmw/resources/vfs"; + } else if (gamePath == "/usr/bin") { + const auto path = "/usr/share/games/openmw/resources/vfs"; + if (std::filesystem::exists(path)) { + // Arch + return path; + } + + // OpenSUSE + return "/usr/share/openmw/resources/vfs"; + } else if (gamePath == "/run/host/usr/bin") { + const auto path = "/run/host/usr/share/games/openmw/resources/vfs"; + if (std::filesystem::exists(path)) { + // Arch from inside a Flatpak sandbox + return path; + } + + // OpenSUSE from inside a Flatpak sandbox + return "/run/host/usr/share/openmw/resources/vfs"; + } else if (boost::ends_with(gamePath.u8string(), + "/app/org.openmw.OpenMW/current/active/files/bin")) { + // Flatpak + return gamePath / "../share/games/openmw/resources/vfs"; + } +#endif + return gamePath / "resources" / "vfs"; +} + +GameType GetGameType(const GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + case GameId::openmw: + return GameType::tes3; + case GameId::tes4: + case GameId::nehrim: + return GameType::tes4; + case GameId::tes5: + case GameId::enderal: + return GameType::tes5; + case GameId::tes5se: + case GameId::enderalse: + return GameType::tes5se; + case GameId::tes5vr: + return GameType::tes5vr; + case GameId::fo3: + return GameType::fo3; + case GameId::fonv: + return GameType::fonv; + case GameId::fo4: + return GameType::fo4; + case GameId::fo4vr: + return GameType::fo4vr; + case GameId::starfield: + return GameType::starfield; + case GameId::oblivionRemastered: + return GameType::oblivionRemastered; + default: + throw std::logic_error("Unrecognised game ID"); + } +} + +float GetMinimumHeaderVersion(const GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + case GameId::openmw: + return MORROWIND_MINIMUM_HEADER_VERSION; + case GameId::tes4: + case GameId::nehrim: + case GameId::oblivionRemastered: + return OBLIVION_MINIMUM_HEADER_VERSION; + case GameId::tes5: + case GameId::enderal: + return SKYRIM_FO3_MINIMUM_HEADER_VERSION; + case GameId::tes5se: + case GameId::tes5vr: + case GameId::enderalse: + return SKYRIM_SE_MINIMUM_HEADER_VERSION; + case GameId::fo3: + return SKYRIM_FO3_MINIMUM_HEADER_VERSION; + case GameId::fonv: + return FONV_MINIMUM_HEADER_VERSION; + case GameId::fo4: + case GameId::fo4vr: + return FO4_MINIMUM_HEADER_VERSION; + case GameId::starfield: + return STARFIELD_MINIMUM_HEADER_VERSION; + default: + throw std::logic_error("Unrecognised game ID"); + } +} + +std::filesystem::path GetDataPath(const GameId gameId, + const std::filesystem::path& gamePath) +{ + switch (gameId) { + case GameId::tes3: + return gamePath / "Data Files"; + case GameId::tes4: + case GameId::nehrim: + case GameId::tes5: + case GameId::enderal: + case GameId::tes5se: + case GameId::enderalse: + case GameId::tes5vr: + case GameId::fo3: + case GameId::fonv: + case GameId::fo4: + case GameId::fo4vr: + case GameId::starfield: + return gamePath / "Data"; + case GameId::openmw: + return GetOpenMWDataPath(gamePath); + case GameId::oblivionRemastered: + return gamePath / "OblivionRemastered" / "Content" / "Dev" / "ObvData" / "Data"; + default: + throw std::logic_error("Unrecognised game ID"); + } +} + +std::string ToString(const GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + return "Morrowind"; + case GameId::tes4: + return "Oblivion"; + case GameId::nehrim: + return "Nehrim"; + case GameId::tes5: + return "Skyrim"; + case GameId::enderal: + return "Enderal"; + case GameId::tes5se: + return "Skyrim Special Edition"; + case GameId::enderalse: + return "Enderal Special Edition"; + case GameId::tes5vr: + return "Skyrim VR"; + case GameId::fo3: + return "Fallout3"; + case GameId::fonv: + return "FalloutNV"; + case GameId::fo4: + return "Fallout4"; + case GameId::fo4vr: + return "Fallout4VR"; + case GameId::starfield: + return "Starfield"; + case GameId::openmw: + return "OpenMW"; + case GameId::oblivionRemastered: + return "Oblivion Remastered"; + default: + throw std::logic_error("Unrecognised game ID"); + } +} + +bool SupportsLightPlugins(const GameType gameType) +{ + return gameType == GameType::tes5se || gameType == GameType::tes5vr || + gameType == GameType::fo4 || gameType == GameType::fo4vr; +} + +std::string GetMasterFilename(const GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + return "Morrowind.esm"; + case GameId::tes4: + case GameId::oblivionRemastered: + return "Oblivion.esm"; + case GameId::nehrim: + return "Nehrim.esm"; + case GameId::tes5: + case GameId::tes5se: + case GameId::tes5vr: + case GameId::enderal: + case GameId::enderalse: + return "Skyrim.esm"; + case GameId::fo3: + return "Fallout3.esm"; + case GameId::fonv: + return "FalloutNV.esm"; + case GameId::fo4: + case GameId::fo4vr: + return "Fallout4.esm"; + case GameId::starfield: + return "Starfield.esm"; + case GameId::openmw: + // This isn't actually a master file, but it's hardcoded to load first, + // and the value is only used to check the game is installed and to + // skip fully loading this file before sorting - and omwscripts files + // don't get loaded anyway. + return "builtin.omwscripts"; + default: + throw std::logic_error("Unrecognised game ID"); + } +} + +std::string GetGameName(const GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + return "TES III: Morrowind"; + case GameId::tes4: + return "TES IV: Oblivion"; + case GameId::nehrim: + return "Nehrim - At Fate's Edge"; + case GameId::tes5: + return "TES V: Skyrim"; + case GameId::enderal: + return "Enderal: Forgotten Stories"; + case GameId::tes5se: + return "TES V: Skyrim Special Edition"; + case GameId::enderalse: + return "Enderal: Forgotten Stories (Special Edition)"; + case GameId::tes5vr: + return "TES V: Skyrim VR"; + case GameId::fo3: + return "Fallout 3"; + case GameId::fonv: + return "Fallout: New Vegas"; + case GameId::fo4: + return "Fallout 4"; + case GameId::fo4vr: + return "Fallout 4 VR"; + case GameId::starfield: + return "Starfield"; + case GameId::openmw: + return "OpenMW"; + case GameId::oblivionRemastered: + return "TES IV: Oblivion Remastered"; + default: + throw std::logic_error("Unrecognised game ID"); + } +} + +std::string GetDefaultMasterlistRepositoryName(const GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + return "morrowind"; + case GameId::tes4: + case GameId::nehrim: + case GameId::oblivionRemastered: + return "oblivion"; + case GameId::tes5: + return "skyrim"; + case GameId::enderal: + case GameId::enderalse: + return "enderal"; + case GameId::tes5se: + return "skyrimse"; + case GameId::tes5vr: + return "skyrimvr"; + case GameId::fo3: + return "fallout3"; + case GameId::fonv: + return "falloutnv"; + case GameId::fo4: + return "fallout4"; + case GameId::fo4vr: + return "fallout4vr"; + case GameId::starfield: + return "starfield"; + default: + throw std::logic_error("Unrecognised game type"); + } +} + +std::string GetDefaultMasterlistUrl(const std::string& repositoryName) +{ + return std::string("https://raw.githubusercontent.com/loot/") + repositoryName + "/" + + DEFAULT_MASTERLIST_BRANCH + "/masterlist.yaml"; +} + +std::string GetDefaultMasterlistUrl(const GameId gameId) +{ + const auto repoName = GetDefaultMasterlistRepositoryName(gameId); + + return GetDefaultMasterlistUrl(repoName); +} + +GameSettings::GameSettings(const GameId gameId, const std::string& lootFolder) + : id_(gameId), type_(GetGameType(gameId)), name_(GetGameName(gameId)), + masterFile_(GetMasterFilename(gameId)), + minimumHeaderVersion_(GetMinimumHeaderVersion(gameId)), + lootFolderName_(lootFolder), masterlistSource_(GetDefaultMasterlistUrl(gameId)) +{} + +bool GameSettings::operator==(const GameSettings& rhs) const +{ + return name_ == rhs.Name() || lootFolderName_ == rhs.FolderName(); +} + +GameId GameSettings::Id() const +{ + return id_; +} + +GameType GameSettings::Type() const +{ + return type_; +} + +std::string GameSettings::Name() const +{ + return name_; +} + +std::string GameSettings::FolderName() const +{ + return lootFolderName_; +} + +std::string GameSettings::Master() const +{ + return masterFile_; +} + +float GameSettings::MinimumHeaderVersion() const +{ + return minimumHeaderVersion_; +} + +std::string GameSettings::MasterlistSource() const +{ + return masterlistSource_; +} + +std::filesystem::path GameSettings::GamePath() const +{ + return gamePath_; +} + +std::filesystem::path GameSettings::GameLocalPath() const +{ + return gameLocalPath_; +} + +std::filesystem::path GameSettings::DataPath() const +{ + return GetDataPath(id_, gamePath_); +} + +GameSettings& GameSettings::SetName(const std::string& name) +{ + name_ = name; + return *this; +} + +GameSettings& GameSettings::SetMaster(const std::string& masterFile) +{ + masterFile_ = masterFile; + return *this; +} + +GameSettings& GameSettings::SetMinimumHeaderVersion(float mininumHeaderVersion) +{ + minimumHeaderVersion_ = mininumHeaderVersion; + return *this; +} + +GameSettings& GameSettings::SetMasterlistSource(const std::string& source) +{ + masterlistSource_ = source; + return *this; +} + +GameSettings& GameSettings::SetGamePath(const std::filesystem::path& path) +{ + gamePath_ = path; + return *this; +} + +GameSettings& GameSettings::SetGameLocalPath(const std::filesystem::path& path) +{ + gameLocalPath_ = path; + return *this; +} + +GameSettings& GameSettings::SetGameLocalFolder(const std::string& folderName) +{ + fs::path appData; +#ifdef _WIN32 + TCHAR path[MAX_PATH]; + + HRESULT res = ::SHGetFolderPath(nullptr, CSIDL_LOCAL_APPDATA, nullptr, + SHGFP_TYPE_CURRENT, path); + if (res == S_OK) { + appData = fs::path(path); + } else { + appData = fs::path(""); + } +#else + const char* xdgData = std::getenv("XDG_DATA_HOME"); + if (xdgData && xdgData[0] != '\0') { + appData = fs::path(xdgData); + } else { + const char* home = std::getenv("HOME"); + if (home && home[0] != '\0') { + appData = fs::path(home) / ".local" / "share"; + } else { + appData = fs::path(""); + } + } +#endif + gameLocalPath_ = appData / fs::path(folderName); + return *this; +} +} // namespace loot diff --git a/libs/lootcli/src/game_settings.h b/libs/lootcli/src/game_settings.h new file mode 100644 index 0000000..bc775c9 --- /dev/null +++ b/libs/lootcli/src/game_settings.h @@ -0,0 +1,101 @@ +#ifndef LOOT_GUI_STATE_GAME_GAME_SETTINGS +#define LOOT_GUI_STATE_GAME_GAME_SETTINGS + +#include +#include +#include +#include +#include + +#include "loot/enum/game_type.h" + +namespace loot +{ +constexpr inline std::string_view NEHRIM_STEAM_REGISTRY_KEY = + "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Steam App " + "1014940\\InstallLocation"; +static constexpr const char* DEFAULT_MASTERLIST_BRANCH = "v0.26"; + +enum struct GameId : uint8_t +{ + tes3, + tes4, + nehrim, + tes5, + enderal, + tes5se, + enderalse, + tes5vr, + fo3, + fonv, + fo4, + fo4vr, + starfield, + openmw, + oblivionRemastered +}; + +GameType GetGameType(const GameId gameId); + +float GetMinimumHeaderVersion(const GameId gameId); + +std::filesystem::path GetDataPath(const GameId gamiId, + const std::filesystem::path& gamePath); + +std::string ToString(const GameId gameId); + +bool SupportsLightPlugins(const GameType gameType); + +std::string GetMasterFilename(const GameId gameId); + +std::string GetGameName(const GameId gameId); + +std::string GetDefaultMasterlistRepositoryName(GameId gameId); + +std::string GetDefaultMasterlistUrl(const std::string& repositoryName); +std::string GetDefaultMasterlistUrl(const GameId gameId); + +class GameSettings +{ +public: + GameSettings() = default; + explicit GameSettings(const GameId gameId, const std::string& lootFolder = ""); + + bool operator==(const GameSettings& rhs) const; // Compares names and folder names. + + GameId Id() const; + GameType Type() const; + std::string Name() const; // Returns the game's name, eg. "TES IV: Oblivion". + std::string FolderName() const; + std::string Master() const; + float MinimumHeaderVersion() const; + std::string MasterlistSource() const; + std::filesystem::path GamePath() const; + std::filesystem::path GameLocalPath() const; + std::filesystem::path DataPath() const; + + GameSettings& SetName(const std::string& name); + GameSettings& SetMaster(const std::string& masterFile); + GameSettings& SetMinimumHeaderVersion(float minimumHeaderVersion); + GameSettings& SetMasterlistSource(const std::string& source); + GameSettings& SetGamePath(const std::filesystem::path& path); + GameSettings& SetGameLocalPath(const std::filesystem::path& GameLocalPath); + GameSettings& SetGameLocalFolder(const std::string& folderName); + +private: + GameId id_{GameId::tes4}; + GameType type_{GameType::tes4}; + std::string name_; + std::string masterFile_; + float minimumHeaderVersion_{0.0f}; + + std::string lootFolderName_; + + std::string masterlistSource_; + + std::filesystem::path gamePath_; // Path to the game's folder. + std::filesystem::path gameLocalPath_; +}; +} // namespace loot + +#endif diff --git a/libs/lootcli/src/lootthread.cpp b/libs/lootcli/src/lootthread.cpp new file mode 100644 index 0000000..9f7f60c --- /dev/null +++ b/libs/lootcli/src/lootthread.cpp @@ -0,0 +1,1197 @@ +#ifdef _WIN32 +#pragma comment(lib, "winhttp.lib") +#endif + +#include "lootthread.h" +#include "game_settings.h" +#include "version.h" +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#endif + +// using namespace loot; +namespace fs = std::filesystem; + +using std::lock_guard; +using std::recursive_mutex; + +namespace lootcli +{ +static const std::set + oldDefaultBranches({"master", "v0.7", "v0.8", "v0.10", "v0.13", "v0.14", "v0.15", + "v0.17", "v0.18", "v0.21"}); +static const std::regex GITHUB_REPO_URL_REGEX = + std::regex(R"(^https://github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$)", + std::regex::ECMAScript | std::regex::icase); + +std::string toString(loot::MessageType type) +{ + switch (type) { + case loot::MessageType::say: + return "info"; + case loot::MessageType::warn: + return "warn"; + case loot::MessageType::error: + return "error"; + default: + return "unknown"; + } +} + +LOOTWorker::LOOTWorker() + : m_GameId(loot::GameId::tes5), m_GameName("Skyrim"), + m_LogLevel(loot::LogLevel::info) +{} + +std::string ToLower(std::string text) +{ + std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + + return text; +} + +void LOOTWorker::setGame(const std::string& gameName) +{ + static std::map gameMap = { + {"morrowind", loot::GameId::tes3}, + {"oblivion", loot::GameId::tes4}, + {"fallout3", loot::GameId::fo3}, + {"fallout4", loot::GameId::fo4}, + {"fallout4vr", loot::GameId::fo4vr}, + {"falloutnv", loot::GameId::fonv}, + {"skyrim", loot::GameId::tes5}, + {"skyrimse", loot::GameId::tes5se}, + {"skyrimvr", loot::GameId::tes5vr}, + {"nehrim", loot::GameId::nehrim}, + {"enderal", loot::GameId::enderal}, + {"enderalse", loot::GameId::enderalse}, + {"starfield", loot::GameId::starfield}, + {"oblivionremastered", loot::GameId::oblivionRemastered}}; + + auto iter = gameMap.find(ToLower(gameName)); + + if (iter != gameMap.end()) { + m_GameId = iter->second; + m_GameName = loot::ToString(m_GameId); + } else { + throw std::runtime_error("invalid game name \"" + gameName + "\""); + } +} + +void LOOTWorker::setGamePath(const std::string& gamePath) +{ + m_GamePath = gamePath; +} + +void LOOTWorker::setOutput(const std::string& outputPath) +{ + m_OutputPath = outputPath; +} + +void LOOTWorker::setUpdateMasterlist(bool update) +{ + m_UpdateMasterlist = update; +} + +void LOOTWorker::setPluginListPath(const std::string& pluginListPath) +{ + m_PluginListPath = pluginListPath; +} + +void LOOTWorker::setLanguageCode(const std::string& languageCode) +{ + m_Language = languageCode; +} + +void LOOTWorker::setLogLevel(loot::LogLevel level) +{ + m_LogLevel = level; +} + +fs::path GetLOOTAppData() +{ +#ifdef _WIN32 + TCHAR path[MAX_PATH]; + + HRESULT res = ::SHGetFolderPath(nullptr, CSIDL_LOCAL_APPDATA, nullptr, + SHGFP_TYPE_CURRENT, path); + + if (res == S_OK) { + return fs::path(path) / "LOOT"; + } else { + return fs::path(""); + } +#else + // On Linux, use XDG_DATA_HOME or fallback to ~/.local/share + const char* xdgData = std::getenv("XDG_DATA_HOME"); + if (xdgData && xdgData[0] != '\0') { + return fs::path(xdgData) / "LOOT"; + } + const char* home = std::getenv("HOME"); + if (home && home[0] != '\0') { + return fs::path(home) / ".local" / "share" / "LOOT"; + } + return fs::path(""); +#endif +} + +fs::path LOOTWorker::gamePath() const +{ + return GetLOOTAppData() / "games" / m_GameSettings.FolderName(); +} + +fs::path LOOTWorker::masterlistPath() const +{ + return gamePath() / "masterlist.yaml"; +} + +fs::path LOOTWorker::userlistPath() const +{ + return gamePath() / "userlist.yaml"; +} +fs::path LOOTWorker::settingsPath() const +{ + return GetLOOTAppData() / "settings.toml"; +} + +fs::path LOOTWorker::l10nPath() const +{ + return GetLOOTAppData() / "resources" / "l10n"; +} + +fs::path LOOTWorker::dataPath() const +{ + return m_GameSettings.DataPath(); +} + +void LOOTWorker::getSettings(const fs::path& file) +{ + lock_guard guard(mutex_); + // Don't use cpptoml::parse_file() as it just uses a std stream, + // which don't support UTF-8 paths on Windows. + std::ifstream in(file); + if (!in.is_open()) + throw std::runtime_error(file.string() + " could not be opened for parsing"); + + const auto settings = toml::parse(in, file.string()); + const auto games = settings["games"]; + if (games.is_array_of_tables()) { + for (const auto& game : *games.as_array()) { + try { + if (!game.is_table()) { + throw std::runtime_error("games array element is not a table"); + } + auto gameTable = *game.as_table(); + + using loot::GameId; + using loot::GameSettings; + + auto id = gameTable["gameId"].value(); + if (!id) { + throw std::runtime_error( + "'gameId' and 'type' keys both missing from game settings table"); + } + const auto gameType = *id; + GameId gameId; + + if (gameType == "Morrowind") { + gameId = GameId::tes3; + } else if (gameType == "Oblivion") { + // The Oblivion game type is shared between Oblivon and Nehrim. + gameId = IsNehrim(gameTable) ? GameId::nehrim : GameId::tes4; + } else if (gameType == "Skyrim") { + // The Skyrim game type is shared between Skyrim and Enderal. + gameId = IsEnderal(gameTable) ? GameId::enderal : GameId::tes5; + } else if (gameType == "SkyrimSE" || gameType == "Skyrim Special Edition") { + // The Skyrim SE game type is shared between Skyrim SE and Enderal SE. + gameId = IsEnderalSE(gameTable) ? GameId::enderalse : GameId::tes5se; + } else if (gameType == "Skyrim VR") { + gameId = GameId::tes5vr; + } else if (gameType == "Fallout3") { + gameId = GameId::fo3; + } else if (gameType == "FalloutNV") { + gameId = GameId::fonv; + } else if (gameType == "Fallout4") { + gameId = GameId::fo4; + } else if (gameType == "Fallout4VR") { + gameId = GameId::fo4vr; + } else if (gameType == "Starfield") { + gameId = GameId::starfield; + } else if (gameType == "OpenMW") { + gameId = GameId::openmw; + } else if (gameType == "Oblivion Remastered") { + gameId = GameId::oblivionRemastered; + } else { + throw std::runtime_error( + "invalid value for 'type' key in game settings table"); + } + + auto folder = gameTable["folder"].value(); + if (!folder) { + throw std::runtime_error("'folder' key missing from game settings table"); + } + + const auto type = gameTable["type"].value(); + + // SkyrimSE was a previous serialised value for GameType::tes5se, + // and the game folder name LOOT created for that game type. + if (type && *type == "SkyrimSE" && *folder == *type) { + folder = "Skyrim Special Edition"; + } + + GameSettings newSettings(gameId, folder.value()); + + if (newSettings.Type() == m_GameSettings.Type()) { + + auto name = gameTable["name"].value(); + if (name) { + newSettings.SetName(*name); + } + + auto master = gameTable["master"].value(); + if (master) { + newSettings.SetMaster(*master); + } + + const auto minimumHeaderVersion = + gameTable["minimumHeaderVersion"].value(); + if (minimumHeaderVersion) { + newSettings.SetMinimumHeaderVersion((float)*minimumHeaderVersion); + } + + auto source = gameTable["masterlistSource"].value(); + if (source) { + newSettings.SetMasterlistSource(migrateMasterlistSource(*source)); + } else { + auto url = gameTable["repo"].value(); + auto branch = gameTable["branch"].value(); + auto migratedSource = + migrateMasterlistRepoSettings(newSettings.Id(), *url, *branch); + if (migratedSource.has_value()) { + newSettings.SetMasterlistSource(migratedSource.value()); + } + } + + auto path = gameTable["path"].value(); + if (path) { + newSettings.SetGamePath(std::filesystem::path(*path)); + } + + auto localPath = gameTable["local_path"].value(); + auto localFolder = gameTable["local_folder"].value(); + if (localPath && localFolder) { + throw std::runtime_error( + "Game settings have local_path and local_folder set, use only one."); + } else if (localPath) { + newSettings.SetGameLocalPath(std::filesystem::path(*localPath)); + } else if (localFolder) { + newSettings.SetGameLocalFolder(*localFolder); + } + + m_GameSettings = newSettings; + break; + } + } catch (...) { + // Skip invalid games. + } + } + } + + if (m_Language.empty()) { + m_Language = settings["language"].value_or(loot::MessageContent::DEFAULT_LANGUAGE); + } +} + +std::optional LOOTWorker::GetLocalFolder(const toml::table& table) +{ + const auto localPath = table["local_path"].value(); + const auto localFolder = table["local_folder"].value(); + + if (localFolder.has_value()) { + return localFolder; + } + + if (localPath.has_value()) { + return std::filesystem::path(*localPath).filename().string(); + } + + return std::nullopt; +} + +bool LOOTWorker::IsNehrim(const toml::table& table) +{ + const auto installPath = table["path"].value(); + + if (installPath.has_value() && !installPath.value().empty()) { + const auto path = std::filesystem::path(installPath.value()); + if (std::filesystem::exists(path)) { + return std::filesystem::exists(path / "NehrimLauncher.exe"); + } + } + + // Fall back to using heuristics based on the existing settings. + // Return true if any of these heuristics return a positive match. + const auto gameName = table["name"].value(); + const auto masterFilename = table["master"].value(); + const auto isBaseGameInstance = table["isBaseGameInstance"].value(); + const auto folder = table["folder"].value(); + + return + // Nehrim uses a different main master file from Oblivion. + (masterFilename.has_value() && + masterFilename.value() == loot::GetMasterFilename(loot::GameId::nehrim)) || + // Game name probably includes "nehrim". + (gameName.has_value() && boost::icontains(gameName.value(), "nehrim")) || + // LOOT folder name probably includes "nehrim". + (folder.has_value() && boost::icontains(folder.value(), "nehrim")) || + // Between 0.18.1 and 0.19.0 inclusive, LOOT had an isBaseGameInstance + // game setting that was false for Nehrim, Enderal and Enderal SE. + (isBaseGameInstance.has_value() && !isBaseGameInstance.value()); +} + +bool LOOTWorker::IsEnderal(const toml::table& table, + const std::string& expectedLocalFolder) +{ + const auto installPath = table["path"].value(); + + if (installPath.has_value() && !installPath.value().empty()) { + const auto path = std::filesystem::path(installPath.value()); + if (std::filesystem::exists(path)) { + return std::filesystem::exists(path / "Enderal Launcher.exe"); + } + } + + // Fall back to using heuristics based on the existing settings. + // Return true if any of these heuristics return a positive match. + const auto gameName = table["name"].value(); + const auto isBaseGameInstance = table["isBaseGameInstance"].value(); + const auto localFolder = GetLocalFolder(table); + const auto folder = table["folder"].value(); + + return + // Enderal and Enderal SE use different local folders than their base + // games. + (localFolder.has_value() && localFolder.value() == expectedLocalFolder) || + // Game name probably includes "enderal". + (gameName.has_value() && boost::icontains(gameName.value(), "enderal")) || + // LOOT folder name probably includes "enderal". + (folder.has_value() && boost::icontains(folder.value(), "enderal")) || + // Between 0.18.1 and 0.19.0 inclusive, LOOT had an isBaseGameInstance + // game setting that was false for Nehrim, Enderal and Enderal SE. + (isBaseGameInstance.has_value() && !isBaseGameInstance.value()); +} + +bool LOOTWorker::IsEnderal(const toml::table& table) +{ + return IsEnderal(table, "enderal"); +} + +bool LOOTWorker::IsEnderalSE(const toml::table& table) +{ + return IsEnderal(table, "Enderal Special Edition"); +} + +std::string LOOTWorker::getOldDefaultRepoUrl(loot::GameId GameId) +{ + switch (GameId) { + case loot::GameId::tes3: + return "https://github.com/loot/morrowind.git"; + case loot::GameId::tes4: + return "https://github.com/loot/oblivion.git"; + case loot::GameId::tes5: + return "https://github.com/loot/skyrim.git"; + case loot::GameId::tes5se: + return "https://github.com/loot/skyrimse.git"; + case loot::GameId::tes5vr: + return "https://github.com/loot/skyrimvr.git"; + case loot::GameId::fo3: + return "https://github.com/loot/fallout3.git"; + case loot::GameId::fonv: + return "https://github.com/loot/falloutnv.git"; + case loot::GameId::fo4: + return "https://github.com/loot/fallout4.git"; + case loot::GameId::fo4vr: + return "https://github.com/loot/fallout4vr.git"; + default: + throw std::runtime_error( + "Unrecognised game type: " + + std::to_string(static_cast>(GameId))); + } +} + +bool LOOTWorker::isLocalPath(const std::string& location, const std::string& filename) +{ + if (boost::starts_with(location, "http://") || + boost::starts_with(location, "https://")) { + return false; + } + + // Could be a local path. Only return true if it points to a non-bare + // Git repository that currently has the given branch checked out and + // the given filename exists in the repo root. + auto locationPath = std::filesystem::path(location); + + auto filePath = locationPath / std::filesystem::path(filename); + + if (!std::filesystem::is_regular_file(filePath)) { + return false; + } + + auto headFilePath = locationPath / ".git" / "HEAD"; + + return std::filesystem::is_regular_file(headFilePath); +} + +bool LOOTWorker::isBranchCheckedOut(const std::filesystem::path& localGitRepo, + const std::string& branch) +{ + auto headFilePath = localGitRepo / ".git" / "HEAD"; + + std::ifstream in(headFilePath); + if (!in.is_open()) { + return false; + } + + std::string line; + std::getline(in, line); + in.close(); + + return line == "ref: refs/heads/" + branch; +} + +std::optional +LOOTWorker::migrateMasterlistRepoSettings(loot::GameId GameId, std::string url, + std::string branch) +{ + + if (oldDefaultBranches.count(branch) == 1) { + // Update to the latest masterlist branch. + log(loot::LogLevel::info, "Updating masterlist repository branch from " + branch + + " to " + loot::DEFAULT_MASTERLIST_BRANCH); + branch = loot::DEFAULT_MASTERLIST_BRANCH; + } + + if (GameId == loot::GameId::tes5vr && url == "https://github.com/loot/skyrimse.git") { + // Switch to the VR-specific repository (introduced for LOOT v0.17.0). + auto newUrl = "https://github.com/loot/skyrimvr.git"; + log(loot::LogLevel::info, + "Updating masterlist repository URL from" + url + " to " + newUrl); + url = newUrl; + } + + if (GameId == loot::GameId::fo4vr && url == "https://github.com/loot/fallout4.git") { + // Switch to the VR-specific repository (introduced for LOOT v0.17.0). + auto newUrl = "https://github.com/loot/fallout4vr.git"; + log(loot::LogLevel::info, + "Updating masterlist repository URL from " + url + " to " + newUrl); + url = newUrl; + } + + auto filename = "masterlist.yaml"; + if (isLocalPath(url, filename)) { + auto localRepoPath = std::filesystem::path(url); + if (!isBranchCheckedOut(localRepoPath, branch)) { + log(loot::LogLevel::warning, + "The URL " + url + + " is a local Git repository path but the configured branch " + branch + + " is not checked out. LOOT will use the path as the masterlist " + "source, but there may be unexpected differences in the loaded " + "metadata if the " + + branch + + " branch is not manually checked out before the " + "next time the masterlist is updated."); + } + + return (localRepoPath / filename).string(); + } + + std::smatch regexMatches; + std::regex_match(url, regexMatches, GITHUB_REPO_URL_REGEX); + if (regexMatches.size() != 3) { + log(loot::LogLevel::warning, + "Cannot migrate masterlist repository settings as the URL does not " + "point to a repository on GitHub."); + return std::nullopt; + } + + auto githubOwner = regexMatches.str(1); + auto githubRepo = regexMatches.str(2); + + return "https://raw.githubusercontent.com/" + githubOwner + "/" + githubRepo + "/" + + branch + "/masterlist.yaml"; +} + +std::string LOOTWorker::migrateMasterlistSource(const std::string& source) +{ + static const std::vector officialMasterlistRepos = { + "morrowind", "oblivion", "skyrim", "skyrimse", "skyrimvr", + "fallout3", "falloutnv", "fallout4", "fallout4vr", "enderal"}; + + for (const auto& repo : officialMasterlistRepos) { + for (const auto& branch : oldDefaultBranches) { + const auto url = "https://raw.githubusercontent.com/loot/" + repo + "/" + branch + + "/masterlist.yaml"; + + if (source == url) { + const auto newSource = loot::GetDefaultMasterlistUrl(repo); + + log(loot::LogLevel::info, + "Migrating masterlist source from " + source + " to " + newSource); + + return newSource; + } + } + } + + return source; +} + +#ifdef _WIN32 +DWORD LOOTWorker::GetFile(const WCHAR* szUrl, // Full URL + const CHAR* szFileName) // Local file name +{ + BYTE szTemp[25]; + DWORD dwSize = 0; + DWORD dwDownloaded = 0; + LPSTR pszOutBuffer; + BOOL bResults = FALSE; + HINTERNET hSession = NULL, hConnect = NULL, hRequest = NULL; + FILE* pFile; + std::wstring_convert> converter; + + URL_COMPONENTS urlComp; + DWORD dwUrlLen = 0; + + DWORD result = ERROR_SUCCESS; + + // Initialize the URL_COMPONENTS structure. + ZeroMemory(&urlComp, sizeof(urlComp)); + urlComp.dwStructSize = sizeof(urlComp); + + // Set required component lengths to non-zero + // so that they are cracked. + wchar_t szHostName[MAX_PATH] = L""; + wchar_t szURLPath[MAX_PATH * 4] = L""; + urlComp.lpszHostName = szHostName; + urlComp.lpszUrlPath = szURLPath; + urlComp.dwSchemeLength = (DWORD)-1; + urlComp.dwHostNameLength = (DWORD)-1; + urlComp.dwUrlPathLength = (DWORD)-1; + urlComp.dwExtraInfoLength = (DWORD)-1; + if (WinHttpCrackUrl(szUrl, (DWORD)wcslen(szUrl), 0, &urlComp)) { + // Use WinHttpOpen to obtain a session handle. + hSession = WinHttpOpen(L"lootcli/1.5.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + + // Specify an HTTP server. + if (hSession) + hConnect = WinHttpConnect(hSession, szHostName, urlComp.nPort, 0); + + // Create an HTTP request handle. + if (hConnect) + hRequest = + WinHttpOpenRequest(hConnect, L"GET", szURLPath, NULL, WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE); + + // Send a request. + if (hRequest) + bResults = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, + WINHTTP_NO_REQUEST_DATA, 0, 0, 0); + + // End the request. + if (bResults) + bResults = WinHttpReceiveResponse(hRequest, NULL); + + // Keep checking for data until there is nothing left. + if (bResults) { + if (!(pFile = fopen(szFileName, "wb"))) { + log(loot::LogLevel::debug, "File open failure"); + result = GetLastError(); + } + do { + // Check for available data. + dwSize = 0; + if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) { + log(loot::LogLevel::debug, "No data"); + result = GetLastError(); + break; + } + + // No more available data. + if (!dwSize) { + log(loot::LogLevel::debug, "No data"); + result = GetLastError(); + break; + } + + // Allocate space for the buffer. + pszOutBuffer = new char[dwSize + 1]; + if (!pszOutBuffer) { + log(loot::LogLevel::debug, "Bad buffer"); + result = GetLastError(); + } + + // Read the Data. + ZeroMemory(pszOutBuffer, dwSize + 1); + + if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) { + log(loot::LogLevel::debug, "Read data failure"); + result = GetLastError(); + } else { + fwrite(pszOutBuffer, sizeof(char), dwSize, pFile); + } + + // Free the memory allocated to the buffer. + delete[] pszOutBuffer; + + // This condition should never be reached since WinHttpQueryDataAvailable + // reported that there are bits to read. + if (!dwDownloaded) + break; + + } while (dwSize > 0); + } else { + log(loot::LogLevel::debug, "Response failure"); + result = GetLastError(); + } + + // Close any open handles. + if (hRequest) + WinHttpCloseHandle(hRequest); + if (hConnect) + WinHttpCloseHandle(hConnect); + if (hSession) + WinHttpCloseHandle(hSession); + fflush(pFile); + fclose(pFile); + } else { + log(loot::LogLevel::debug, "URL parse failure: " + converter.to_bytes(szUrl)); + result = GetLastError(); + } + return result; +} +#else +// Linux implementation using libcurl +static size_t curlWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) +{ + FILE* fp = static_cast(userp); + return fwrite(contents, size, nmemb, fp); +} + +int LOOTWorker::GetFile(const std::string& url, const std::string& fileName) +{ + CURL* curl = curl_easy_init(); + if (!curl) { + log(loot::LogLevel::error, "Failed to initialize curl"); + return 1; + } + + FILE* fp = fopen(fileName.c_str(), "wb"); + if (!fp) { + log(loot::LogLevel::debug, "File open failure: " + fileName); + curl_easy_cleanup(curl); + return 1; + } + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "lootcli/" LOOTCLI_VERSION_STRING); + + CURLcode res = curl_easy_perform(curl); + + fflush(fp); + fclose(fp); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + log(loot::LogLevel::error, + std::string("curl download failed: ") + curl_easy_strerror(res)); + return 1; + } + + return 0; +} +#endif + +std::string escape(const std::string& s) +{ + return boost::replace_all_copy(s, "\"", "\\\""); +} + +int LOOTWorker::run() +{ + m_startTime = std::chrono::high_resolution_clock::now(); + + { + // Do some preliminary locale / UTF-8 support setup here, in case the settings file + // reading requires it. + // Boost.Locale initialisation: Specify location of language dictionaries. + boost::locale::generator gen; + gen.add_messages_path(l10nPath().string()); + gen.add_messages_domain("loot"); + + // Boost.Locale initialisation: Generate and imbue locales. + std::locale::global(gen("en.UTF-8")); + } + + loot::SetLoggingCallback([&](loot::LogLevel level, std::string_view message) { + log(level, message); + }); + + try { + fs::path profile(m_PluginListPath); + profile = profile.parent_path(); + + m_GameSettings = loot::GameSettings(m_GameId, loot::ToString(m_GameId)); + + fs::path settings = settingsPath(); + + if (fs::exists(settings)) + getSettings(settings); + + m_GameSettings.SetGamePath(m_GamePath); + + std::unique_ptr gameHandle = CreateGameHandle( + m_GameSettings.Type(), m_GameSettings.GamePath(), profile.string()); + + if (!GetLOOTAppData().empty()) { + // Make sure that the LOOT game path exists. + auto lootGamePath = gamePath(); + if (!fs::is_directory(lootGamePath)) { + if (fs::exists(lootGamePath)) { + throw std::runtime_error( + "Could not create LOOT folder for game, the path exists but is not " + "a directory"); + } + + std::vector legacyGamePaths{GetLOOTAppData() / + fs::path(m_GameSettings.FolderName())}; + + if (m_GameSettings.Id() == loot::GameId::tes5se) { + // LOOT v0.10.0 used SkyrimSE as its folder name for Skyrim SE, so + // migrate from that if it's present. + legacyGamePaths.insert(legacyGamePaths.begin(), + GetLOOTAppData() / "SkyrimSE"); + } + + for (const auto& legacyGamePath : legacyGamePaths) { + if (fs::is_directory(legacyGamePath)) { + log(loot::LogLevel::info, + "Found a folder for this game in the LOOT data folder, " + "assuming " + "that it's a legacy game folder and moving into the correct " + "subdirectory..."); + + fs::create_directories(lootGamePath.parent_path()); + fs::rename(legacyGamePath, lootGamePath); + break; + } + } + + fs::create_directories(lootGamePath); + } + } + + if (m_Language != loot::MessageContent::DEFAULT_LANGUAGE) { + log(loot::LogLevel::debug, "initialising language settings"); + log(loot::LogLevel::debug, "selected language: " + m_Language); + + // Boost.Locale initialisation: Generate and imbue locales. + boost::locale::generator gen; + std::locale::global(gen(m_Language + ".UTF-8")); + } + + progress(Progress::CheckingMasterlistExistence); + if (!fs::exists(masterlistPath())) { + if (!m_UpdateMasterlist) { + log(loot::LogLevel::error, + "Masterlist not found at: " + masterlistPath().string()); + return 0; // was FALSE on Windows + } + fs::create_directories(masterlistPath().parent_path()); + } + + if (m_UpdateMasterlist) { + progress(Progress::UpdatingMasterlist); + +#ifdef _WIN32 + std::wstring_convert> converter; + std::wstring masterlistSource = + converter.from_bytes(m_GameSettings.MasterlistSource()); + + log(loot::LogLevel::info, "Downloading latest masterlist file from " + + m_GameSettings.MasterlistSource() + " to " + + masterlistPath().string()); + DWORD result = + GetFile(masterlistSource.c_str(), masterlistPath().string().c_str()); + if (result != ERROR_SUCCESS) { + LPVOID lpMsgBuf; + LPVOID lpDisplayBuf; + LPCWSTR lpszFunction = TEXT("GetFile"); + DWORD dw = result; + + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR)&lpMsgBuf, 0, NULL); + + lpDisplayBuf = + (LPVOID)LocalAlloc(LMEM_ZEROINIT, (lstrlen((LPCTSTR)lpMsgBuf) + + lstrlen((LPCTSTR)lpszFunction) + 40) * + sizeof(TCHAR)); + StringCchPrintf((LPTSTR)lpDisplayBuf, LocalSize(lpDisplayBuf) / sizeof(TCHAR), + TEXT("%s failed with error %d: %s"), lpszFunction, dw, + lpMsgBuf); + + std::wstring errorMessage = (LPTSTR)lpDisplayBuf; + + log(loot::LogLevel::error, + "Error downloading masterlist: " + converter.to_bytes(errorMessage)); + return 0; // was FALSE + } +#else + log(loot::LogLevel::info, "Downloading latest masterlist file from " + + m_GameSettings.MasterlistSource() + " to " + + masterlistPath().string()); + int result = GetFile(m_GameSettings.MasterlistSource(), + masterlistPath().string()); + if (result != 0) { + log(loot::LogLevel::error, "Error downloading masterlist"); + return 0; + } +#endif + } + + progress(Progress::LoadingLists); + + gameHandle->GetDatabase().LoadMasterlist(masterlistPath().string()); + fs::path userlist = userlistPath(); + if (fs::exists(userlist)) + gameHandle->GetDatabase().LoadUserlist(userlist.string()); + + progress(Progress::ReadingPlugins); + gameHandle->LoadCurrentLoadOrderState(); + auto loadOrder = gameHandle->GetLoadOrder(); + std::vector pluginsList; + for (auto plugin : gameHandle->GetLoadOrder()) { + std::filesystem::path pluginPath(plugin); + pluginsList.push_back(pluginPath); + } + gameHandle->LoadPlugins(pluginsList, false); + + progress(Progress::SortingPlugins); + std::vector sortedPlugins = gameHandle->SortPlugins(loadOrder); + + progress(Progress::WritingLoadorder); + + std::ofstream outf(m_PluginListPath); + if (!outf) { + log(loot::LogLevel::error, + "failed to open " + m_PluginListPath + " to rewrite it"); + return 1; + } + outf << "# This file was automatically generated by Mod Organizer." << std::endl; + for (const std::string& plugin : sortedPlugins) { + outf << plugin << std::endl; + } + outf.close(); + + progress(Progress::ParsingLootMessages); + std::ofstream(m_OutputPath) << createJsonReport(*gameHandle, sortedPlugins); + } catch (std::system_error& e) { + log(loot::LogLevel::error, e.what()); + return 1; + } catch (const std::exception& e) { + log(loot::LogLevel::error, e.what()); + return 1; + } + + progress(Progress::Done); + + return 0; +} + +void set(QJsonObject& o, const char* e, const QJsonValue& v) +{ + if (v.isObject() && v.toObject().isEmpty()) { + return; + } + + if (v.isArray() && v.toArray().isEmpty()) { + return; + } + + if (v.isString() && v.toString().isEmpty()) { + return; + } + + o[e] = v; +} + +std::string +LOOTWorker::createJsonReport(loot::GameInterface& game, + const std::vector& sortedPlugins) const +{ + QJsonObject root; + + set(root, "messages", createMessages(game.GetDatabase().GetGeneralMessages(true))); + set(root, "plugins", createPlugins(game, sortedPlugins)); + + const auto end = std::chrono::high_resolution_clock::now(); + + set(root, "stats", + QJsonObject{{"time", static_cast(std::chrono::duration_cast( + end - m_startTime) + .count())}, + {"lootcliVersion", LOOTCLI_VERSION_STRING}, + {"lootVersion", QString::fromStdString(loot::GetLiblootVersion())}}); + + QJsonDocument doc(root); + return doc.toJson(QJsonDocument::Indented).toStdString(); +} + +template +QJsonArray createStringArray(const Container& c) +{ + QJsonArray array; + + for (auto&& e : c) { + array.push_back(QString::fromStdString(e)); + } + + return array; +} + +QJsonArray +LOOTWorker::createPlugins(loot::GameInterface& game, + const std::vector& sortedPlugins) const +{ + QJsonArray plugins; + + for (auto&& pluginName : sortedPlugins) { + + auto plugin = game.GetPlugin(pluginName); + + QJsonObject o; + o["name"] = QString::fromStdString(pluginName); + + if (auto metaData = game.GetDatabase().GetPluginMetadata(pluginName, true, true)) { + set(o, "incompatibilities", + createIncompatibilities(game, metaData->GetIncompatibilities())); + set(o, "messages", createMessages(metaData->GetMessages())); + set(o, "dirty", createDirty(metaData->GetDirtyInfo())); + set(o, "clean", createClean(metaData->GetCleanInfo())); + } + + set(o, "missingMasters", createMissingMasters(game, pluginName)); + + if (plugin->LoadsArchive()) { + o["loadsArchive"] = true; + } + + if (plugin->IsMaster()) { + o["isMaster"] = true; + } + + if (plugin->IsLightPlugin()) { + o["isLightMaster"] = true; + } + + // don't add if the name is the only thing in there + if (o.size() > 1) { + plugins.push_back(o); + } + } + + return plugins; +} + +QJsonValue LOOTWorker::createMessages(const std::vector& list) const +{ + QJsonArray messages; + + for (loot::Message m : list) { + auto simpleMessage = loot::SelectMessageContent(m.GetContent(), m_Language); + if (simpleMessage.has_value()) { + messages.push_back(QJsonObject{ + {"type", QString::fromStdString(toString(m.GetType()))}, + {"text", QString::fromStdString(simpleMessage.value().GetText())}}); + } + } + + return messages; +} + +QJsonValue +LOOTWorker::createDirty(const std::vector& data) const +{ + QJsonArray array; + + for (const auto& d : data) { + QJsonObject o{ + {"crc", static_cast(d.GetCRC())}, + {"itm", static_cast(d.GetITMCount())}, + {"deletedReferences", static_cast(d.GetDeletedReferenceCount())}, + {"deletedNavmesh", static_cast(d.GetDeletedNavmeshCount())}, + }; + + set(o, "cleaningUtility", QString::fromStdString(d.GetCleaningUtility())); + auto simpleMessage = loot::SelectMessageContent( + loot::Message(loot::MessageType::say, d.GetDetail()).GetContent(), m_Language); + if (simpleMessage.has_value()) { + set(o, "info", QString::fromStdString(simpleMessage.value().GetText())); + } else { + set(o, "info", QString::fromStdString("")); + } + + array.push_back(o); + } + + return array; +} + +QJsonValue +LOOTWorker::createClean(const std::vector& data) const +{ + QJsonArray array; + + for (const auto& d : data) { + QJsonObject o{ + {"crc", static_cast(d.GetCRC())}, + }; + + set(o, "cleaningUtility", QString::fromStdString(d.GetCleaningUtility())); + auto simpleMessage = loot::SelectMessageContent( + loot::Message(loot::MessageType::say, d.GetDetail()).GetContent(), m_Language); + if (simpleMessage.has_value()) { + set(o, "info", QString::fromStdString(simpleMessage.value().GetText())); + } else { + set(o, "info", QString::fromStdString("")); + } + + array.push_back(o); + } + + return array; +} + +QJsonValue +LOOTWorker::createIncompatibilities(loot::GameInterface& game, + const std::vector& data) const +{ + QJsonArray array; + + for (auto&& f : data) { + const auto n = static_cast(f.GetName()); + if (!game.GetPlugin(n)) { + continue; + } + + const auto name = QString::fromStdString(n); + const auto displayName = QString::fromStdString(f.GetDisplayName()); + + QJsonObject o{{"name", name}}; + + if (displayName != name) { + set(o, "displayName", displayName); + } + + array.push_back(std::move(o)); + } + + return array; +} + +QJsonValue LOOTWorker::createMissingMasters(loot::GameInterface& game, + const std::string& pluginName) const +{ + QJsonArray array; + + for (auto&& master : game.GetPlugin(pluginName)->GetMasters()) { + if (!game.GetPlugin(master)) { + array.push_back(QString::fromStdString(master)); + } + } + + return array; +} + +void LOOTWorker::progress(Progress p) +{ + std::cout << "[progress] " << static_cast(p) << "\n"; + std::cout.flush(); +} + +void LOOTWorker::log(loot::LogLevel level, const std::string_view message) const +{ + if (level < m_LogLevel) { + return; + } + + const auto ll = fromLootLogLevel(level); + const auto levelName = logLevelToString(ll); + + std::cout << "[" << levelName << "] " << message << "\n"; + std::cout.flush(); +} + +loot::LogLevel toLootLogLevel(lootcli::LogLevels level) +{ + using L = loot::LogLevel; + using LC = lootcli::LogLevels; + + switch (level) { + case LC::Trace: + return L::trace; + case LC::Debug: + return L::debug; + case LC::Info: + return L::info; + case LC::Warning: + return L::warning; + case LC::Error: + return L::error; + default: + return L::info; + } +} + +lootcli::LogLevels fromLootLogLevel(loot::LogLevel level) +{ + using L = loot::LogLevel; + using LC = lootcli::LogLevels; + + switch (level) { + case L::trace: + return LC::Trace; + + case L::debug: + return LC::Debug; + + case L::info: + return LC::Info; + + case L::warning: + return LC::Warning; + + case L::error: + return LC::Error; + + default: + return LC::Info; + } +} + +} // namespace lootcli diff --git a/libs/lootcli/src/lootthread.h b/libs/lootcli/src/lootthread.h new file mode 100644 index 0000000..d4f4e65 --- /dev/null +++ b/libs/lootcli/src/lootthread.h @@ -0,0 +1,107 @@ +#ifndef LOOTTHREAD_H +#define LOOTTHREAD_H + +#include "game_settings.h" +#include "loot/database_interface.h" +#include + +namespace loot +{ +class Game; +} + +namespace lootcli +{ + +loot::LogLevel toLootLogLevel(lootcli::LogLevels level); +lootcli::LogLevels fromLootLogLevel(loot::LogLevel level); + +class LOOTWorker +{ +public: + explicit LOOTWorker(); + + void setGame(const std::string& gameName); + void setGamePath(const std::string& gamePath); + void setOutput(const std::string& outputPath); + void setPluginListPath(const std::string& pluginListPath); + void + setLanguageCode(const std::string& language_code); // Will add this when I figure out + // how languages work on MO + void setLogLevel(loot::LogLevel level); + + void setUpdateMasterlist(bool update); + + int run(); + +private: + void progress(Progress p); + void log(loot::LogLevel level, const std::string_view message) const; + +#ifdef _WIN32 + DWORD GetFile(const WCHAR* szUrl, const CHAR* szFileName); +#else + int GetFile(const std::string& url, const std::string& fileName); +#endif + void getSettings(const std::filesystem::path& file); + std::string getOldDefaultRepoUrl(loot::GameId gameType); + std::optional GetLocalFolder(const toml::table& table); + bool IsNehrim(const toml::table& table); + bool IsEnderal(const toml::table& table, const std::string& expectedLocalFolder); + bool IsEnderal(const toml::table& table); + bool IsEnderalSE(const toml::table& table); + bool isLocalPath(const std::string& location, const std::string& filename); + bool isBranchCheckedOut(const std::filesystem::path& localGitRepo, + const std::string& branch); + std::optional migrateMasterlistRepoSettings(loot::GameId gameType, + std::string url, + std::string branch); + std::string migrateMasterlistSource(const std::string& source); + + std::filesystem::path gamePath() const; + std::filesystem::path masterlistPath() const; + std::filesystem::path settingsPath() const; + std::filesystem::path userlistPath() const; + std::filesystem::path l10nPath() const; + std::filesystem::path dataPath() const; + +private: + // void handleErr(unsigned int resultCode, const char *description); + bool sort(loot::Game& game); + // const char *lootErrorString(unsigned int errorCode); + // template T resolveVariable(HMODULE lib, const char *name); + // template T resolveFunction(HMODULE lib, const char *name); + +private: + loot::GameId m_GameId; + std::string m_Language; + std::string m_GameName; + std::string m_GamePath; + std::string m_OutputPath; + std::string m_PluginListPath; + loot::LogLevel m_LogLevel; + bool m_UpdateMasterlist; + mutable std::recursive_mutex mutex_; + loot::GameSettings m_GameSettings; + std::chrono::high_resolution_clock::time_point m_startTime; + + std::string createJsonReport(loot::GameInterface& game, + const std::vector& sortedPlugins) const; + + QJsonArray createPlugins(loot::GameInterface& game, + const std::vector& sortedPlugins) const; + + QJsonValue createMessages(const std::vector& list) const; + QJsonValue createDirty(const std::vector& data) const; + QJsonValue createClean(const std::vector& data) const; + + QJsonValue createIncompatibilities(loot::GameInterface& game, + const std::vector& data) const; + + QJsonValue createMissingMasters(loot::GameInterface& game, + const std::string& pluginName) const; +}; + +} // namespace lootcli + +#endif // LOOTTHREAD_H diff --git a/libs/lootcli/src/main.cpp b/libs/lootcli/src/main.cpp new file mode 100644 index 0000000..d06e0ef --- /dev/null +++ b/libs/lootcli/src/main.cpp @@ -0,0 +1,102 @@ +#include "lootthread.h" +#include + +using namespace std; + +template +T getParameter(const std::vector& arguments, const std::string& key) +{ + auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key); + if ((iter != arguments.end()) && ((iter + 1) != arguments.end())) { + return boost::lexical_cast(*(iter + 1)); + } else { + throw std::runtime_error(std::string("argument missing " + key)); + } +} + +template <> +bool getParameter(const std::vector& arguments, + const std::string& key) +{ + auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key); + if (iter != arguments.end()) { + return true; + } else { + return false; + } +} + +template +T getOptionalParameter(const std::vector& arguments, + const std::string& key, T def) +{ + try { + return getParameter(arguments, key); + } catch (std::runtime_error&) { + return def; + } +} + +loot::LogLevel getLogLevel(const std::vector& arguments) +{ + const auto s = getOptionalParameter(arguments, "logLevel", ""); + const auto level = lootcli::logLevelFromString(s); + + return lootcli::toLootLogLevel(level); +} + +#ifdef _WIN32 +int wWinMain(HINSTANCE, HINSTANCE, LPTSTR, int) +{ + _setmode(_fileno(stdout), _O_BINARY); + setlocale(LC_ALL, "en.UTF-8"); + + std::vector arguments; + int argc; + LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); + + if (argv) { + for (int i = 0; i < argc; ++i) { + size_t num_converted; + std::vector arg(wcslen(argv[i]) * sizeof(wchar_t) + 1); + + wcstombs_s(&num_converted, &(arg[0]), arg.size(), argv[i], arg.size() - 1); + + arguments.push_back(&(arg[0])); + } + } +#else +int main(int argc, char* argv[]) +{ + setlocale(LC_ALL, "en_US.UTF-8"); + + std::vector arguments; + for (int i = 0; i < argc; ++i) { + arguments.push_back(argv[i]); + } +#endif + + // design rationale: this was designed to have the actual loot stuff run in a separate + // thread. That turned out to be unnecessary atm. + + try { + lootcli::LOOTWorker worker; + + worker.setUpdateMasterlist(!getParameter(arguments, "skipUpdateMasterlist")); + worker.setGame(getParameter(arguments, "game")); + worker.setGamePath(getParameter(arguments, "gamePath")); + worker.setPluginListPath(getParameter(arguments, "pluginListPath")); + worker.setOutput(getParameter(arguments, "out")); + worker.setLogLevel(getLogLevel(arguments)); + + const auto lang = getOptionalParameter(arguments, "language", ""); + if (!lang.empty()) { + worker.setLanguageCode(lang); + } + + return worker.run(); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what(); + return 1; + } +} diff --git a/libs/lootcli/src/pch.cpp b/libs/lootcli/src/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/libs/lootcli/src/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/libs/lootcli/src/pch.h b/libs/lootcli/src/pch.h new file mode 100644 index 0000000..cf460fd --- /dev/null +++ b/libs/lootcli/src/pch.h @@ -0,0 +1,84 @@ +#ifdef _MSC_VER +#pragma warning(disable : 4251) // neds to have dll-interface +#pragma warning(disable : 4355) // this used in initializer list +#pragma warning(disable : 4371) // layout may have changed +#pragma warning(disable : 4514) // unreferenced inline function removed +#pragma warning(disable : 4571) // catch semantics changed +#pragma warning(disable : 4619) // no warning X +#pragma warning(disable : 4623) // default constructor deleted +#pragma warning(disable : 4625) // copy constructor deleted +#pragma warning(disable : 4626) // copy assignment operator deleted +#pragma warning(disable : 4710) // function not inlined +#pragma warning(disable : 4820) // padding +#pragma warning(disable : 4866) // left-to-right evaluation order +#pragma warning(disable : 4868) // left-to-right evaluation order +#pragma warning(disable : 5026) // move constructor deleted +#pragma warning(disable : 5027) // move assignment operator deleted +#pragma warning(disable : 5045) // spectre mitigation + +#pragma warning(push, 3) +#pragma warning(disable : 4365) // signed/unsigned mismatch +#pragma warning(disable : 4774) // bad format string +#pragma warning(disable : 4946) // reinterpret_cast used between related classes +#pragma warning(disable : 4800) // implicit conversion +#endif + +// std +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// qt +#include +#include +#include +#include +#include + +// boost +#include +#include +#include + +// loot +#include +#include + +// third-party +#include + +#ifdef _WIN32 +// windows +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#else +// Linux equivalents +#include +#include +#endif + +#ifdef _MSC_VER +#undef _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING +#pragma warning(pop) +#endif diff --git a/libs/lootcli/src/version.h b/libs/lootcli/src/version.h new file mode 100644 index 0000000..d496a56 --- /dev/null +++ b/libs/lootcli/src/version.h @@ -0,0 +1,4 @@ +#define LOOTCLI_VERSION_MAJOR 1 +#define LOOTCLI_VERSION_MINOR 7 +#define LOOTCLI_VERSION_MAINTENANCE 0 +#define LOOTCLI_VERSION_STRING "1.7.0" diff --git a/libs/lootcli/src/version.rc b/libs/lootcli/src/version.rc new file mode 100644 index 0000000..bf6ebb4 --- /dev/null +++ b/libs/lootcli/src/version.rc @@ -0,0 +1,35 @@ +#include "version.h" +#include "Winver.h" + +#define VER_FILEVERSION LOOTCLI_VERSION_MAJOR,LOOTCLI_VERSION_MINOR,LOOTCLI_VERSION_MAINTENANCE,0 +#define VER_FILEVERSION_STR LOOTCLI_VERSION_STRING + +VS_VERSION_INFO VERSIONINFO +FILEVERSION VER_FILEVERSION +PRODUCTVERSION VER_FILEVERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS (0) +FILEOS VOS__WINDOWS32 +FILETYPE VFT_APP +FILESUBTYPE (0) +BEGIN +BLOCK "StringFileInfo" +BEGIN +BLOCK "040904B0" +BEGIN +VALUE "FileVersion", VER_FILEVERSION_STR +VALUE "CompanyName", "Mod Organizer 2 Team\0" +VALUE "FileDescription", "LOOT Command line interface\0" +VALUE "OriginalFilename", "lootcli.exe\0" +VALUE "InternalName", "lootcli\0" +VALUE "LegalCopyright", "Copyright 2011-2016 Sebastian Herbord\r\nCopyright 2016-2025 Mod Organizer 2 contributors\0" +VALUE "ProductName", "lootcli\0" +VALUE "ProductVersion", VER_FILEVERSION_STR +END +END + +BLOCK "VarFileInfo" +BEGIN +VALUE "Translation", 0x0409, 1200 +END +END diff --git a/libs/lootcli/vcpkg.json b/libs/lootcli/vcpkg.json new file mode 100644 index 0000000..d890106 --- /dev/null +++ b/libs/lootcli/vcpkg.json @@ -0,0 +1,30 @@ +{ + "dependencies": ["boost-locale", "tomlplusplus", "libloot"], + "overrides": [ + { + "name": "tomlplusplus", + "version": "3.1.0" + } + ], + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43", + "packages": ["boost*", "boost-*"] + }, + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673", + "packages": ["libloot"] + } + ] + } +} diff --git a/libs/lzokay.py b/libs/lzokay.py new file mode 100644 index 0000000..c967219 --- /dev/null +++ b/libs/lzokay.py @@ -0,0 +1,6 @@ +# Linux compatibility shim for optional lzokay dependency used by some +# basic_games modules (e.g. STALKER Anomaly save parsing). + + +def decompress(_data, _size=None): + raise RuntimeError("lzokay is not installed") diff --git a/libs/nak_ffi/CMakeLists.txt b/libs/nak_ffi/CMakeLists.txt new file mode 100644 index 0000000..6fde501 --- /dev/null +++ b/libs/nak_ffi/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.16) +project(nak_ffi) + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(CARGO_BUILD_TYPE "debug") + set(CARGO_BUILD_FLAGS "") +else() + set(CARGO_BUILD_TYPE "release") + set(CARGO_BUILD_FLAGS "--release") +endif() + +set(NAK_FFI_DIR ${CMAKE_CURRENT_SOURCE_DIR}) +set(NAK_FFI_LIB ${NAK_FFI_DIR}/target/${CARGO_BUILD_TYPE}/libnak_ffi.so) + +add_custom_command( + OUTPUT ${NAK_FFI_LIB} + COMMAND cargo build ${CARGO_BUILD_FLAGS} + WORKING_DIRECTORY ${NAK_FFI_DIR} + COMMENT "Building NaK FFI library (Rust)" + DEPENDS ${NAK_FFI_DIR}/Cargo.toml ${NAK_FFI_DIR}/src/lib.rs +) + +add_custom_target(nak_ffi_build DEPENDS ${NAK_FFI_LIB}) + +# INTERFACE wrapper so ALIAS works (CMake can't alias IMPORTED targets) +add_library(nak_ffi_wrapper INTERFACE) +target_link_libraries(nak_ffi_wrapper INTERFACE ${NAK_FFI_LIB}) +target_include_directories(nak_ffi_wrapper INTERFACE ${NAK_FFI_DIR}/include) +add_dependencies(nak_ffi_wrapper nak_ffi_build) +add_library(mo2::nak_ffi ALIAS nak_ffi_wrapper) + +install(FILES ${NAK_FFI_LIB} DESTINATION lib) +install(DIRECTORY ${NAK_FFI_DIR}/include/ DESTINATION include) diff --git a/libs/nak_ffi/Cargo.lock b/libs/nak_ffi/Cargo.lock new file mode 100644 index 0000000..af0ac00 --- /dev/null +++ b/libs/nak_ffi/Cargo.lock @@ -0,0 +1,2784 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e4850548ff4a25a77ce3bda7241874e17fb702ea551f0cc62a2dbe052f1272" +dependencies = [ + "anstyle", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "serde", + "unty", +] + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "cfg_aliases", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "num-traits", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "log", + "slotmap", + "tinyvec", + "ttf-parser 0.25.1", +] + +[[package]] +name = "fontdue" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b" +dependencies = [ + "hashbrown 0.15.5", + "rayon", + "ttf-parser 0.21.1", +] + +[[package]] +name = "fontique" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bbc252c93499b6d3635d692f892a637db0dbb130ce9b32bf20b28e0dcc470b" +dependencies = [ + "bytemuck", + "hashbrown 0.16.1", + "icu_locale_core", + "linebender_resource_handle", + "memmap2", + "objc2", + "objc2-core-foundation", + "objc2-core-text", + "objc2-foundation", + "read-fonts", + "roxmltree", + "smallvec", + "windows", + "windows-core 0.58.0", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", + "rayon", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "i-slint-common" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7df69e8ff23fd605ac12112e31844f92b83e5958235f3ec446efe79bc868e65" +dependencies = [ + "fontique", + "ttf-parser 0.25.1", +] + +[[package]] +name = "i-slint-compiler" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09f34ca3843cae3e92c1a8a4167680e4e4a2f72ea16bcbded9bc06271393427" +dependencies = [ + "annotate-snippets", + "by_address", + "derive_more", + "fontdue", + "i-slint-common", + "image", + "itertools", + "linked_hash_set", + "lyon_extra", + "lyon_path", + "num_enum", + "proc-macro2", + "quote", + "rayon", + "resvg", + "rowan", + "rspolib", + "smol_str", + "strum", + "typed-index-collections", + "url", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.0", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core 0.5.1", + "zune-jpeg 0.5.12", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kurbo" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linked_hash_set" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lyon_extra" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca94c7bf1e2557c2798989c43416822c12fc5dcc5e17cc3307ef0e71894a955" +dependencies = [ + "lyon_path", + "thiserror 1.0.69", +] + +[[package]] +name = "lyon_geom" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e260b6de923e6e47adfedf6243013a7a874684165a6a277594ee3906021b2343" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aeca86bcfd632a15984ba029b539ffb811e0a70bf55e814ef8b0f54f506fdeb" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "nak_ffi" +version = "0.1.0" +dependencies = [ + "nak_rust", +] + +[[package]] +name = "nak_rust" +version = "4.4.1" +source = "git+https://github.com/SulfurNitride/NaK.git?branch=main#ceea41b64e6cafb8375978d7982e2c46fe826047" +dependencies = [ + "chrono", + "rand", + "serde", + "serde_json", + "slint-build", + "ureq", + "walkdir", +] + +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "objc2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "resvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg 0.5.12", +] + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rowan" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash", + "text-size", +] + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rspolib" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fda9a7796aff63a7b1b39ccc93fffaaf65e20042984b4843041a49ca4677535" +dependencies = [ + "lazy_static", + "natord", + "snafu", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser 0.25.1", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slint-build" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81baa6950be442989aedda43bc4318a174ebf902a50b1be4c1f8488cdfe1b023" +dependencies = [ + "derive_more", + "i-slint-compiler", + "spin_on", + "toml_edit 0.24.0+spec-1.1.0", +] + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol_str" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7a918bd2a9951d18ee6e48f076843e8e73a9a5d22cf05bcd4b7a81bdd04e17" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "spin_on" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076e103ed41b9864aa838287efe5f4e3a7a0362dd00671ae62a212e5e4612da2" +dependencies = [ + "pin-utils", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.24.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c740b185920170a6d9191122cafef7010bd6270a3824594bff6784c04d7f09e" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typed-index-collections" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898160f1dfd383b4e92e17f0512a7d62f3c51c44937b23b6ffc3a1614a8eaccd" +dependencies = [ + "bincode", + "serde", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "usvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" +dependencies = [ + "zune-core 0.5.1", +] diff --git a/libs/nak_ffi/Cargo.toml b/libs/nak_ffi/Cargo.toml new file mode 100644 index 0000000..000297c --- /dev/null +++ b/libs/nak_ffi/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "nak_ffi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +nak_rust = { git = "https://github.com/SulfurNitride/NaK.git", branch = "main", default-features = false, features = ["installer"] } diff --git a/libs/nak_ffi/include/nak_ffi.h b/libs/nak_ffi/include/nak_ffi.h new file mode 100644 index 0000000..c6010d4 --- /dev/null +++ b/libs/nak_ffi/include/nak_ffi.h @@ -0,0 +1,220 @@ +#ifndef NAK_FFI_H +#define NAK_FFI_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ======================================================================== + * Tier 1: Game Detection + * ======================================================================== */ + +/** A detected game installation */ +typedef struct { + char *name; + char *app_id; + char *install_path; + char *prefix_path; /* NULL if no prefix */ + char *launcher; /* display name string */ + char *my_games_folder; /* NULL if not applicable */ + char *appdata_local_folder; /* NULL if not applicable */ + char *appdata_roaming_folder; /* NULL if not applicable */ + char *registry_path; /* NULL if not applicable */ + char *registry_value; /* NULL if not applicable */ +} NakGame; + +/** List of detected games */ +typedef struct { + NakGame *games; + size_t count; + size_t steam_count; + size_t heroic_count; + size_t bottles_count; +} NakGameList; + +/** Detect all installed games across all launchers */ +NakGameList nak_detect_all_games(void); + +/** Free a NakGameList returned by nak_detect_all_games */ +void nak_game_list_free(NakGameList list); + +/** A known game definition (static data, do NOT free) */ +typedef struct { + const char *name; + const char *steam_app_id; + const char *gog_app_id; /* NULL if none */ + const char *my_games_folder; /* NULL if not applicable */ + const char *appdata_local_folder; /* NULL if not applicable */ + const char *appdata_roaming_folder; /* NULL if not applicable */ + const char *registry_path; + const char *registry_value; + const char *steam_folder; +} NakKnownGame; + +/** Get the list of all known games (static data, do NOT free). + * Returns pointer to array; writes count to *out_count. */ +const NakKnownGame *nak_get_known_games(size_t *out_count); + +/* ======================================================================== + * Tier 2: Proton Detection + * ======================================================================== */ + +/** An installed Proton version */ +typedef struct { + char *name; + char *config_name; + char *path; + int is_steam_proton; + int is_experimental; +} NakSteamProton; + +/** List of detected Proton installations */ +typedef struct { + NakSteamProton *protons; + size_t count; +} NakProtonList; + +/** Find all installed Proton versions */ +NakProtonList nak_find_steam_protons(void); + +/** Free a NakProtonList */ +void nak_proton_list_free(NakProtonList list); + +/* ======================================================================== + * Tier 3: Steam Shortcuts + * ======================================================================== */ + +/** Result from adding a Steam shortcut */ +typedef struct { + uint32_t app_id; + char *prefix_path; /* NULL on error */ + char *error; /* NULL on success */ +} NakShortcutResult; + +/** Add a mod manager as a non-Steam game shortcut. + * Check result.error: NULL = success. */ +NakShortcutResult nak_add_mod_manager_shortcut( + const char *name, + const char *exe_path, + const char *start_dir, + const char *proton_name +); + +/** Remove a non-Steam game shortcut by AppID. + * Returns NULL on success, or error message (free with nak_string_free). */ +char *nak_remove_steam_shortcut(uint32_t app_id); + +/** Free a NakShortcutResult */ +void nak_shortcut_result_free(NakShortcutResult result); + +/* ======================================================================== + * Tier 4: Steam Paths + * ======================================================================== */ + +/** Find the Steam installation path. + * Returns newly allocated string (free with nak_string_free), or NULL. */ +char *nak_find_steam_path(void); + +/* ======================================================================== + * Tier 5: Managed Prefixes + * ======================================================================== */ + +/** A managed Wine prefix */ +typedef struct { + uint32_t app_id; + char *name; + char *prefix_path; + char *install_path; + char *manager_type; + char *library_path; + char *created; /* ISO 8601 timestamp */ + char *proton_config_name; /* NULL if not set */ +} NakManagedPrefix; + +/** List of managed prefixes */ +typedef struct { + NakManagedPrefix *prefixes; + size_t count; +} NakManagedPrefixList; + +/** Load all managed prefixes */ +NakManagedPrefixList nak_managed_prefixes_load(void); + +/** Register a new managed prefix (proton_config_name may be NULL) */ +void nak_managed_prefixes_register( + uint32_t app_id, + const char *name, + const char *prefix_path, + const char *install_path, + const char *library_path, + const char *proton_config_name +); + +/** Unregister a managed prefix by AppID */ +void nak_managed_prefixes_unregister(uint32_t app_id); + +/** Free a NakManagedPrefixList */ +void nak_managed_prefix_list_free(NakManagedPrefixList list); + +/* ======================================================================== + * Tier 6: Dependency Installation (callback-based) + * ======================================================================== */ + +/** Callback for status/log messages */ +typedef void (*NakStatusCallback)(const char *message); +typedef void (*NakLogCallback)(const char *message); + +/** Callback for progress updates (0.0 to 1.0) */ +typedef void (*NakProgressCallback)(float progress); + +/** Install all Wine prefix dependencies (blocking call). + * cancel_flag: pointer to int, set non-zero to cancel. + * Returns NULL on success, or error message (free with nak_string_free). */ +char *nak_install_all_dependencies( + const char *prefix_path, + const char *proton_name, + const char *proton_path, + NakStatusCallback status_cb, + NakLogCallback log_cb, + NakProgressCallback progress_cb, + const int *cancel_flag, + uint32_t app_id +); + +/** Apply Wine registry settings to a prefix. + * Returns NULL on success, or error message (free with nak_string_free). */ +char *nak_apply_wine_registry_settings( + const char *prefix_path, + const char *proton_name, + const char *proton_path, + NakLogCallback log_cb, + uint32_t app_id +); + +/* ======================================================================== + * Tier 7: Prefix Symlinks + * ======================================================================== */ + +/** Ensure AppData/Local/Temp exists in the Wine prefix. + * Call during prefix creation. */ +void nak_ensure_temp_directory(const char *prefix_path); + +/** Detect games and create symlinks from the prefix to game prefixes. + * Call during prefix creation. */ +void nak_create_game_symlinks_auto(const char *prefix_path); + +/* ======================================================================== + * General + * ======================================================================== */ + +/** Free a string returned by any nak_* function */ +void nak_string_free(char *s); + +#ifdef __cplusplus +} +#endif + +#endif /* NAK_FFI_H */ diff --git a/libs/nak_ffi/src/lib.rs b/libs/nak_ffi/src/lib.rs new file mode 100644 index 0000000..0e7b4f3 --- /dev/null +++ b/libs/nak_ffi/src/lib.rs @@ -0,0 +1,692 @@ +//! NaK FFI - C bindings for NaK game detection and Proton management +//! +//! Memory management rules: +//! - Owned strings returned as `*mut c_char` must be freed with `nak_string_free()` +//! - Struct lists (NakGameList, etc.) must be freed with their corresponding `_free()` fn +//! - Error returns: functions returning `*mut c_char` for errors use null = success +//! - `NakKnownGame` pointers are static data and must NOT be freed + +use std::ffi::{c_char, c_float, c_int, CStr, CString}; +use std::path::Path; +use std::ptr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, LazyLock, Mutex}; + +// ============================================================================ +// Helper functions +// ============================================================================ + +fn to_cstring(s: &str) -> *mut c_char { + CString::new(s).unwrap_or_default().into_raw() +} + +fn to_cstring_opt(s: Option<&str>) -> *mut c_char { + match s { + Some(s) => to_cstring(s), + None => ptr::null_mut(), + } +} + +unsafe fn from_cstr<'a>(p: *const c_char) -> &'a str { + if p.is_null() { + "" + } else { + unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("") + } +} + +fn error_to_cstring(e: Box) -> *mut c_char { + to_cstring(&e.to_string()) +} + +// ============================================================================ +// Tier 1: Game Detection +// ============================================================================ + +/// A detected game installation (C-compatible) +#[repr(C)] +pub struct NakGame { + pub name: *mut c_char, + pub app_id: *mut c_char, + pub install_path: *mut c_char, + pub prefix_path: *mut c_char, // null if no prefix + pub launcher: *mut c_char, // display name string + pub my_games_folder: *mut c_char, + pub appdata_local_folder: *mut c_char, + pub appdata_roaming_folder: *mut c_char, + pub registry_path: *mut c_char, + pub registry_value: *mut c_char, +} + +/// List of detected games +#[repr(C)] +pub struct NakGameList { + pub games: *mut NakGame, + pub count: usize, + pub steam_count: usize, + pub heroic_count: usize, + pub bottles_count: usize, +} + +#[derive(Clone)] +struct CachedGame { + name: String, + app_id: String, + install_path: String, + prefix_path: Option, + launcher: String, + my_games_folder: Option, + appdata_local_folder: Option, + appdata_roaming_folder: Option, + registry_path: Option, + registry_value: Option, +} + +#[derive(Clone, Default)] +struct CachedGameList { + games: Vec, + steam_count: usize, + heroic_count: usize, + bottles_count: usize, +} + +static DETECTED_GAMES_CACHE: LazyLock>> = + LazyLock::new(|| Mutex::new(None)); + +fn detect_games_cached() -> CachedGameList { + let mut cache = DETECTED_GAMES_CACHE.lock().unwrap(); + if let Some(cached) = cache.as_ref() { + return cached.clone(); + } + + let result = nak_rust::game_finder::detect_all_games(); + let cached = CachedGameList { + games: result + .games + .iter() + .map(|g| CachedGame { + name: g.name.clone(), + app_id: g.app_id.clone(), + install_path: g.install_path.to_string_lossy().into_owned(), + prefix_path: g + .prefix_path + .as_ref() + .map(|p| p.to_string_lossy().into_owned()), + launcher: g.launcher.display_name().to_string(), + my_games_folder: g.my_games_folder.clone(), + appdata_local_folder: g.appdata_local_folder.clone(), + appdata_roaming_folder: g.appdata_roaming_folder.clone(), + registry_path: g.registry_path.clone(), + registry_value: g.registry_value.clone(), + }) + .collect(), + steam_count: result.steam_count, + heroic_count: result.heroic_count, + bottles_count: result.bottles_count, + }; + + *cache = Some(cached.clone()); + cached +} + +/// Detect all installed games across all launchers +#[no_mangle] +pub extern "C" fn nak_detect_all_games() -> NakGameList { + let result = detect_games_cached(); + + let mut games: Vec = result + .games + .iter() + .map(|g| NakGame { + name: to_cstring(&g.name), + app_id: to_cstring(&g.app_id), + install_path: to_cstring(&g.install_path), + prefix_path: match &g.prefix_path { + Some(p) => to_cstring(p), + None => ptr::null_mut(), + }, + launcher: to_cstring(&g.launcher), + my_games_folder: to_cstring_opt(g.my_games_folder.as_deref()), + appdata_local_folder: to_cstring_opt(g.appdata_local_folder.as_deref()), + appdata_roaming_folder: to_cstring_opt(g.appdata_roaming_folder.as_deref()), + registry_path: to_cstring_opt(g.registry_path.as_deref()), + registry_value: to_cstring_opt(g.registry_value.as_deref()), + }) + .collect(); + + let list = NakGameList { + games: games.as_mut_ptr(), + count: games.len(), + steam_count: result.steam_count, + heroic_count: result.heroic_count, + bottles_count: result.bottles_count, + }; + std::mem::forget(games); + list +} + +/// Free a NakGameList returned by nak_detect_all_games +#[no_mangle] +pub unsafe extern "C" fn nak_game_list_free(list: NakGameList) { + if list.games.is_null() { + return; + } + let games = unsafe { Vec::from_raw_parts(list.games, list.count, list.count) }; + for g in games { + free_if_nonnull(g.name); + free_if_nonnull(g.app_id); + free_if_nonnull(g.install_path); + free_if_nonnull(g.prefix_path); + free_if_nonnull(g.launcher); + free_if_nonnull(g.my_games_folder); + free_if_nonnull(g.appdata_local_folder); + free_if_nonnull(g.appdata_roaming_folder); + free_if_nonnull(g.registry_path); + free_if_nonnull(g.registry_value); + } +} + +unsafe fn free_if_nonnull(p: *mut c_char) { + if !p.is_null() { + let _ = unsafe { CString::from_raw(p) }; + } +} + +/// A known game definition (static data, do NOT free) +#[repr(C)] +pub struct NakKnownGame { + pub name: *const c_char, + pub steam_app_id: *const c_char, + pub gog_app_id: *const c_char, // null if none + pub my_games_folder: *const c_char, + pub appdata_local_folder: *const c_char, + pub appdata_roaming_folder: *const c_char, + pub registry_path: *const c_char, + pub registry_value: *const c_char, + pub steam_folder: *const c_char, +} + +// We need to leak CStrings for the static known games list since the Rust statics +// are &str, not null-terminated. We build the list once and leak it. +// Raw pointers in NakKnownGame prevent Send/Sync, so we wrap in a newtype. +struct KnownGamesVec(Vec); +// SAFETY: The leaked CStrings are effectively 'static and immutable after initialization. +unsafe impl Send for KnownGamesVec {} +unsafe impl Sync for KnownGamesVec {} + +static KNOWN_GAMES_FFI: std::sync::LazyLock = std::sync::LazyLock::new(|| { + KnownGamesVec( + nak_rust::game_finder::KNOWN_GAMES + .iter() + .map(|kg| NakKnownGame { + name: leak_str(kg.name), + steam_app_id: leak_str(kg.steam_app_id), + gog_app_id: leak_str_opt(kg.gog_app_id), + my_games_folder: leak_str_opt(kg.my_games_folder), + appdata_local_folder: leak_str_opt(kg.appdata_local_folder), + appdata_roaming_folder: leak_str_opt(kg.appdata_roaming_folder), + registry_path: leak_str(kg.registry_path), + registry_value: leak_str(kg.registry_value), + steam_folder: leak_str(kg.steam_folder), + }) + .collect(), + ) +}); + +fn leak_str(s: &str) -> *const c_char { + CString::new(s).unwrap_or_default().into_raw() as *const c_char +} + +fn leak_str_opt(s: Option<&str>) -> *const c_char { + match s { + Some(s) => leak_str(s), + None => ptr::null(), + } +} + +/// Get the list of all known games (static data, do NOT free) +/// +/// Returns a pointer to the first element and writes the count to `out_count`. +#[no_mangle] +pub unsafe extern "C" fn nak_get_known_games(out_count: *mut usize) -> *const NakKnownGame { + let games = &KNOWN_GAMES_FFI.0; + if !out_count.is_null() { + *out_count = games.len(); + } + games.as_ptr() +} + +// ============================================================================ +// Tier 2: Proton Detection +// ============================================================================ + +/// An installed Proton version (C-compatible) +#[repr(C)] +pub struct NakSteamProton { + pub name: *mut c_char, + pub config_name: *mut c_char, + pub path: *mut c_char, + pub is_steam_proton: c_int, + pub is_experimental: c_int, +} + +/// List of detected Proton installations +#[repr(C)] +pub struct NakProtonList { + pub protons: *mut NakSteamProton, + pub count: usize, +} + +/// Find all installed Proton versions +#[no_mangle] +pub extern "C" fn nak_find_steam_protons() -> NakProtonList { + let protons = nak_rust::steam::find_steam_protons(); + + let mut ffi_protons: Vec = protons + .iter() + .map(|p| NakSteamProton { + name: to_cstring(&p.name), + config_name: to_cstring(&p.config_name), + path: to_cstring(&p.path.to_string_lossy()), + is_steam_proton: p.is_steam_proton as c_int, + is_experimental: p.is_experimental as c_int, + }) + .collect(); + + let list = NakProtonList { + protons: ffi_protons.as_mut_ptr(), + count: ffi_protons.len(), + }; + std::mem::forget(ffi_protons); + list +} + +/// Free a NakProtonList +#[no_mangle] +pub unsafe extern "C" fn nak_proton_list_free(list: NakProtonList) { + if list.protons.is_null() { + return; + } + let protons = unsafe { Vec::from_raw_parts(list.protons, list.count, list.count) }; + for p in protons { + free_if_nonnull(p.name); + free_if_nonnull(p.config_name); + free_if_nonnull(p.path); + } +} + +// ============================================================================ +// Tier 3: Steam Shortcuts +// ============================================================================ + +/// Result from adding a Steam shortcut +#[repr(C)] +pub struct NakShortcutResult { + pub app_id: u32, + pub prefix_path: *mut c_char, + pub error: *mut c_char, // null on success +} + +/// Add a mod manager as a non-Steam game shortcut +/// +/// Returns a NakShortcutResult. Check `error` field - null means success. +#[no_mangle] +pub unsafe extern "C" fn nak_add_mod_manager_shortcut( + name: *const c_char, + exe_path: *const c_char, + start_dir: *const c_char, + proton_name: *const c_char, +) -> NakShortcutResult { + let name = unsafe { from_cstr(name) }; + let exe = unsafe { from_cstr(exe_path) }; + let dir = unsafe { from_cstr(start_dir) }; + let proton = unsafe { from_cstr(proton_name) }; + + match nak_rust::steam::add_mod_manager_shortcut(name, exe, dir, proton, None, false) { + Ok(result) => NakShortcutResult { + app_id: result.app_id, + prefix_path: to_cstring(&result.prefix_path.to_string_lossy()), + error: ptr::null_mut(), + }, + Err(e) => NakShortcutResult { + app_id: 0, + prefix_path: ptr::null_mut(), + error: error_to_cstring(e), + }, + } +} + +/// Remove a non-Steam game shortcut by AppID +/// +/// Returns null on success, or an error message string (caller must free with nak_string_free). +#[no_mangle] +pub unsafe extern "C" fn nak_remove_steam_shortcut(app_id: u32) -> *mut c_char { + match nak_rust::steam::remove_steam_shortcut(app_id) { + Ok(()) => ptr::null_mut(), + Err(e) => error_to_cstring(e), + } +} + +/// Free a NakShortcutResult +#[no_mangle] +pub unsafe extern "C" fn nak_shortcut_result_free(result: NakShortcutResult) { + free_if_nonnull(result.prefix_path); + free_if_nonnull(result.error); +} + +// ============================================================================ +// Tier 4: Steam Paths +// ============================================================================ + +/// Find the Steam installation path +/// +/// Returns a newly allocated string (caller must free with nak_string_free), +/// or null if Steam is not found. +#[no_mangle] +pub extern "C" fn nak_find_steam_path() -> *mut c_char { + match nak_rust::steam::find_steam_path() { + Some(path) => to_cstring(&path.to_string_lossy()), + None => ptr::null_mut(), + } +} + +// ============================================================================ +// Tier 5: Managed Prefixes +// ============================================================================ + +/// A managed Wine prefix (C-compatible) +#[repr(C)] +pub struct NakManagedPrefix { + pub app_id: u32, + pub name: *mut c_char, + pub prefix_path: *mut c_char, + pub install_path: *mut c_char, + pub manager_type: *mut c_char, + pub library_path: *mut c_char, + pub created: *mut c_char, + pub proton_config_name: *mut c_char, // null if not set +} + +/// List of managed prefixes +#[repr(C)] +pub struct NakManagedPrefixList { + pub prefixes: *mut NakManagedPrefix, + pub count: usize, +} + +/// Load all managed prefixes +#[no_mangle] +pub extern "C" fn nak_managed_prefixes_load() -> NakManagedPrefixList { + let managed = nak_rust::config::ManagedPrefixes::load(); + + let mut ffi_prefixes: Vec = managed + .prefixes + .iter() + .map(|p| NakManagedPrefix { + app_id: p.app_id, + name: to_cstring(&p.name), + prefix_path: to_cstring(&p.prefix_path), + install_path: to_cstring(&p.install_path), + manager_type: to_cstring(&p.manager_type.to_string()), + library_path: to_cstring(&p.library_path), + created: to_cstring(&p.created.to_rfc3339()), + proton_config_name: to_cstring_opt(p.proton_config_name.as_deref()), + }) + .collect(); + + let list = NakManagedPrefixList { + prefixes: ffi_prefixes.as_mut_ptr(), + count: ffi_prefixes.len(), + }; + std::mem::forget(ffi_prefixes); + list +} + +/// Register a new managed prefix +#[no_mangle] +pub unsafe extern "C" fn nak_managed_prefixes_register( + app_id: u32, + name: *const c_char, + prefix_path: *const c_char, + install_path: *const c_char, + library_path: *const c_char, + proton_config_name: *const c_char, +) { + let name = unsafe { from_cstr(name) }; + let prefix = unsafe { from_cstr(prefix_path) }; + let install = unsafe { from_cstr(install_path) }; + let library = unsafe { from_cstr(library_path) }; + let proton = if proton_config_name.is_null() { + None + } else { + let s = unsafe { from_cstr(proton_config_name) }; + if s.is_empty() { + None + } else { + Some(s) + } + }; + + nak_rust::config::ManagedPrefixes::register( + app_id, + name, + prefix, + install, + nak_rust::config::ManagerType::MO2, + library, + proton, + ); +} + +/// Unregister a managed prefix by AppID +#[no_mangle] +pub extern "C" fn nak_managed_prefixes_unregister(app_id: u32) { + nak_rust::config::ManagedPrefixes::unregister(app_id); +} + +/// Free a NakManagedPrefixList +#[no_mangle] +pub unsafe extern "C" fn nak_managed_prefix_list_free(list: NakManagedPrefixList) { + if list.prefixes.is_null() { + return; + } + let prefixes = unsafe { Vec::from_raw_parts(list.prefixes, list.count, list.count) }; + for p in prefixes { + free_if_nonnull(p.name); + free_if_nonnull(p.prefix_path); + free_if_nonnull(p.install_path); + free_if_nonnull(p.manager_type); + free_if_nonnull(p.library_path); + free_if_nonnull(p.created); + free_if_nonnull(p.proton_config_name); + } +} + +// ============================================================================ +// Tier 6: Dependency Installation (callback-based) +// ============================================================================ + +/// Callback for status messages: fn(message: *const c_char) +pub type NakStatusCallback = Option; + +/// Callback for log messages: fn(message: *const c_char) +pub type NakLogCallback = Option; + +/// Callback for progress updates: fn(progress: f32) where 0.0..=1.0 +pub type NakProgressCallback = Option; + +/// Install all Wine prefix dependencies (winetricks, .NET, registry, etc.) +/// +/// This is a blocking call. Use callbacks for progress updates. +/// `cancel_flag` should point to an int that can be set to non-zero to cancel. +/// +/// Returns null on success, or an error message (caller must free with nak_string_free). +#[no_mangle] +pub unsafe extern "C" fn nak_install_all_dependencies( + prefix_path: *const c_char, + proton_name: *const c_char, + proton_path: *const c_char, + status_cb: NakStatusCallback, + log_cb: NakLogCallback, + progress_cb: NakProgressCallback, + cancel_flag: *const c_int, + app_id: u32, +) -> *mut c_char { + let prefix = unsafe { from_cstr(prefix_path) }; + let _proton_name = unsafe { from_cstr(proton_name) }; + let proton_path_str = unsafe { from_cstr(proton_path) }; + + // Find the matching SteamProton by path + let protons = nak_rust::steam::find_steam_protons(); + let proton = match protons + .iter() + .find(|p| p.path.to_string_lossy() == proton_path_str) + { + Some(p) => p.clone(), + None => { + return to_cstring(&format!( + "Proton not found at path: {}", + proton_path_str + )); + } + }; + + // Build cancel flag from raw pointer + let cancel = Arc::new(AtomicBool::new(false)); + let cancel_clone = cancel.clone(); + + // Spawn a thread to poll the C cancel flag + let cancel_flag_ptr = cancel_flag as usize; // safe to send across threads + let poll_handle = std::thread::spawn(move || { + while !cancel_clone.load(Ordering::Relaxed) { + std::thread::sleep(std::time::Duration::from_millis(100)); + if cancel_flag_ptr != 0 { + let flag = unsafe { *(cancel_flag_ptr as *const c_int) }; + if flag != 0 { + cancel_clone.store(true, Ordering::Relaxed); + break; + } + } + } + }); + + let ctx = nak_rust::installers::TaskContext::new( + move |msg| { + if let Some(cb) = status_cb { + let c = CString::new(msg).unwrap_or_default(); + unsafe { cb(c.as_ptr()) }; + } + }, + move |msg| { + if let Some(cb) = log_cb { + let c = CString::new(msg).unwrap_or_default(); + unsafe { cb(c.as_ptr()) }; + } + }, + move |p| { + if let Some(cb) = progress_cb { + unsafe { cb(p) }; + } + }, + cancel.clone(), + ); + + let result = nak_rust::installers::install_all_dependencies( + Path::new(prefix), + &proton, + &ctx, + 0.0, + 1.0, + app_id, + ); + + // Stop the cancel polling thread + cancel.store(true, Ordering::Relaxed); + let _ = poll_handle.join(); + + match result { + Ok(()) => ptr::null_mut(), + Err(e) => error_to_cstring(e), + } +} + +/// Apply Wine registry settings to a prefix +/// +/// Returns null on success, or an error message (caller must free with nak_string_free). +#[no_mangle] +pub unsafe extern "C" fn nak_apply_wine_registry_settings( + prefix_path: *const c_char, + proton_name: *const c_char, + proton_path: *const c_char, + log_cb: NakLogCallback, + app_id: u32, +) -> *mut c_char { + let prefix = unsafe { from_cstr(prefix_path) }; + let _proton_name = unsafe { from_cstr(proton_name) }; + let proton_path_str = unsafe { from_cstr(proton_path) }; + + let protons = nak_rust::steam::find_steam_protons(); + let proton = match protons + .iter() + .find(|p| p.path.to_string_lossy() == proton_path_str) + { + Some(p) => p.clone(), + None => { + return to_cstring(&format!( + "Proton not found at path: {}", + proton_path_str + )); + } + }; + + let log_fn = move |msg: String| { + if let Some(cb) = log_cb { + let c = CString::new(msg).unwrap_or_default(); + unsafe { cb(c.as_ptr()) }; + } + }; + + let app_id_opt = if app_id == 0 { None } else { Some(app_id) }; + + match nak_rust::installers::apply_wine_registry_settings( + Path::new(prefix), + &proton, + &log_fn, + app_id_opt, + ) { + Ok(()) => ptr::null_mut(), + Err(e) => error_to_cstring(e), + } +} + +// ============================================================================ +// Tier 7: Prefix Symlinks +// ============================================================================ + +/// Ensure the Temp directory exists in the Wine prefix's AppData/Local. +/// +/// MO2 and other tools require AppData/Local/Temp to exist. +#[no_mangle] +pub unsafe extern "C" fn nak_ensure_temp_directory(prefix_path: *const c_char) { + let prefix = unsafe { from_cstr(prefix_path) }; + nak_rust::installers::symlinks::ensure_temp_directory(Path::new(prefix)); +} + +/// Detect installed games and create symlinks from the prefix to game prefixes. +/// +/// This is a convenience wrapper that detects games and creates symlinks in one call. +#[no_mangle] +pub unsafe extern "C" fn nak_create_game_symlinks_auto(prefix_path: *const c_char) { + let prefix = unsafe { from_cstr(prefix_path) }; + nak_rust::installers::symlinks::create_game_symlinks_auto(Path::new(prefix)); +} + +// ============================================================================ +// General: String free +// ============================================================================ + +/// Free a string returned by any nak_* function +#[no_mangle] +pub unsafe extern "C" fn nak_string_free(s: *mut c_char) { + free_if_nonnull(s); +} diff --git a/libs/plugin_python/.clang-format b/libs/plugin_python/.clang-format new file mode 100644 index 0000000..c040316 --- /dev/null +++ b/libs/plugin_python/.clang-format @@ -0,0 +1,21 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 4 +--- +Language: Cpp +# Force pointers to the type for C++. +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -4 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: All +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Stroustrup +ColumnLimit: 88 diff --git a/libs/plugin_python/.git-blame-ignore-revs b/libs/plugin_python/.git-blame-ignore-revs new file mode 100644 index 0000000..6f8a3f9 --- /dev/null +++ b/libs/plugin_python/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +1bffcfa8bfba93a9505692b11fdd3f0903983542 +7108d59267b0fe4e9442f9bb146ea5d70e7704a8 diff --git a/libs/plugin_python/.gitattributes b/libs/plugin_python/.gitattributes new file mode 100644 index 0000000..e613fd7 --- /dev/null +++ b/libs/plugin_python/.gitattributes @@ -0,0 +1,8 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf +*.py text eol=crlf diff --git a/libs/plugin_python/.github/workflows/build-and-test.yml b/libs/plugin_python/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..57e7475 --- /dev/null +++ b/libs/plugin_python/.github/workflows/build-and-test.yml @@ -0,0 +1,38 @@ +name: Build & Test Plugin Python + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }} + +jobs: + build: + runs-on: windows-2022 + steps: + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Configure Plugin Python + id: configure-plugin-python + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase + mo2-skip-build: true + + - name: Build Plugin Python + working-directory: ${{ steps.configure-plugin-python.outputs.working-directory }} + run: cmake --build vsbuild --config RelWithDebInfo --verbose ` + --target python-tests --target runner-tests --target proxy + + - name: Test Plugin Python + working-directory: ${{ steps.configure-plugin-python.outputs.working-directory }} + run: ctest --test-dir vsbuild -C RelWithDebInfo --output-on-failure + + - name: Install Plugin Python + working-directory: ${{ steps.configure-plugin-python.outputs.working-directory }} + run: cmake --build vsbuild --config RelWithDebInfo --target INSTALL diff --git a/libs/plugin_python/.github/workflows/linting.yml b/libs/plugin_python/.github/workflows/linting.yml new file mode 100644 index 0000000..70b5bca --- /dev/null +++ b/libs/plugin_python/.github/workflows/linting.yml @@ -0,0 +1,24 @@ +name: Lint Plugin Python + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." + - uses: actions/setup-python@v4 + with: + python-version: "3.12" + - uses: abatilo/actions-poetry@v2 + - name: Check format Python tests + run: | + poetry install --no-root + poetry run poe lint diff --git a/libs/plugin_python/.gitignore b/libs/plugin_python/.gitignore new file mode 100644 index 0000000..5fc3c13 --- /dev/null +++ b/libs/plugin_python/.gitignore @@ -0,0 +1,12 @@ +# build +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build + +# python +tests/**/__pycache__ + +# IDE +.vscode diff --git a/libs/plugin_python/.pre-commit-config.yaml b/libs/plugin_python/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/plugin_python/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/plugin_python/CMakeLists.txt b/libs/plugin_python/CMakeLists.txt new file mode 100644 index 0000000..411a82e --- /dev/null +++ b/libs/plugin_python/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.16) + +cmake_policy(SET CMP0144 NEW) + +project(plugin_python CXX) +set(MO2_QT_VERSION_MAJOR 6) + +find_package(Python COMPONENTS Interpreter Development REQUIRED) +find_package(pybind11 CONFIG REQUIRED) + +get_filename_component(Python_HOME ${Python_EXECUTABLE} PATH) +set(Python_DLL_DIR "${Python_HOME}") +execute_process( + COMMAND ${Python_EXECUTABLE} -c "import sysconfig; print(sysconfig.get_path('stdlib'))" + OUTPUT_VARIABLE Python_LIB_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) +execute_process( + COMMAND ${Python_EXECUTABLE} -c "import sysconfig; print(sysconfig.get_path('purelib'))" + OUTPUT_VARIABLE Python_PURELIB_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) +execute_process( + COMMAND ${Python_EXECUTABLE} -c "import sysconfig; print(sysconfig.get_path('platlib'))" + OUTPUT_VARIABLE Python_PLATLIB_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +set(Python_SHARED_LIBRARY "") +if(DEFINED Python_LIBRARIES AND Python_LIBRARIES) + list(GET Python_LIBRARIES 0 Python_SHARED_LIBRARY) +elseif(DEFINED Python_LIBRARY_RELEASE AND Python_LIBRARY_RELEASE) + set(Python_SHARED_LIBRARY "${Python_LIBRARY_RELEASE}") +elseif(DEFINED Python_LIBRARY AND Python_LIBRARY) + set(Python_SHARED_LIBRARY "${Python_LIBRARY}") +endif() + +mo2_python_install_pyqt() + +# useful for naming DLL, zip, etc. (3.10 -> 310) +set(Python_VERSION_SHORT ${Python_VERSION_MAJOR}${Python_VERSION_MINOR}) + +# projects +add_subdirectory(src) + +# tests (if requested) +set(BUILD_TESTING ${BUILD_TESTING} CACHE BOOL "build tests for plugin_python") +if (BUILD_TESTING) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/libs/plugin_python/CMakePresets.json b/libs/plugin_python/CMakePresets.json new file mode 100644 index 0000000..6293933 --- /dev/null +++ b/libs/plugin_python/CMakePresets.json @@ -0,0 +1,73 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "testing" + } + }, + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone;testing" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/plugin_python/README.md b/libs/plugin_python/README.md new file mode 100644 index 0000000..9085360 --- /dev/null +++ b/libs/plugin_python/README.md @@ -0,0 +1,68 @@ +# ModOrganizer2 - Python Proxy + +This repository contains the Python proxy plugin for ModOrganizer2. +The proxy plugin allow developers to write Python plugins for ModOrganizer2. + +## Setup, build, tests + +This repository is part of MO2 main repositories and should usually be build using +[`mob`](https://github.com/ModOrganizer2/mob). + +## Organization + +This repositories contains 5 sub-projects in `src`. +The interface between Python and C++ is done using the +[`pybind11`](https://github.com/pybind/pybind11) library. +See the `README` in the subfolder (when there is one) for more details. + +- [`src/proxy`](src/proxy/) contains the actual proxy plugin. + This project is a simple interface between MO2 and the runner (see below). + The CMake code: + - generates the `plugin_python.dll` library, + - generates the translation file (under `src/`), + - installs necessary files for the plugin (Python DLL, Python libraries, etc), + including `mobase`. +- [`src/runner`](src/runner/) contains the Python runner. This is the project that + instantiates a Python interpreter and load/unload Python plugins. +- [`src/pybind11-qt`](src/pybind11-qt/) contains many utility stuff to interface + pybind11 with Qt and PyQt. +- [`src/pybind11-utils`](src/pybind11-utils/) contains some utility stuff pybind11. + This project is header-only. +- [`src/mobase`](src/mobase) contains the Python plugin interface. + - This projects generates the `mobase` Python library. + +Some (woefully incomplete) tests are available under `tests`, split in three +sub-directories: + +- [`tests/mocks`](tests/mocks/) simply contains mocks of `uibase` interfaces to be used + in the two other test projects. +- [`tests/python`](tests/python/) contains Python tests for `pytest`. + This project generates a bunch of Python test libraries that are then imported in + Python test files (`test_*.py`) and tested using `pytest`. + These tests mostly cover the pybind11 Qt and utility stuff, and some standalone + MO2 classes and functions (`IFileTree`, `GuessedString`, etc). +- [`tests/runner`](tests/runner/) contains C++ tests, using GTest + Tests in this project instantiate a Python runner and then use it to check that + plugins implemented in Python can be used properly on the C++ side. + +## Building & Running tests + +Tests are not built by default with `mob`, so you will need to run `cmake` manually +with the proper arguments. + +You need to define `PLUGIN_PYTHON_TESTS` with `-DPLUGIN_PYTHON_TESTS` when running +the configure step of cmake. + +You can then build the tests + +```bash +# replace vsbuild with your build folder +cmake --build vsbuild --config RelWithDebInfo --target "python-tests" "runner-tests" +``` + +To run the tests, use `ctest` + +```bash +# replace vsbuild with your build folder +ctest.exe --test-dir vsbuild -C RelWithDebInfo +``` diff --git a/libs/plugin_python/poetry.lock b/libs/plugin_python/poetry.lock new file mode 100644 index 0000000..0ceb7f6 --- /dev/null +++ b/libs/plugin_python/poetry.lock @@ -0,0 +1,254 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mobase-stubs" +version = "2.5.1a0" +description = "PEP561 stub files for the mobase Python API." +optional = false +python-versions = "<4.0,>=3.12" +files = [ + {file = "mobase_stubs-2.5.1a0-py3-none-any.whl", hash = "sha256:bcaecfae038b890d82280fc518f7e44f38d22d35801a8ba7ffa480f7756d6823"}, + {file = "mobase_stubs-2.5.1a0.tar.gz", hash = "sha256:a8dc5574336ed3b1f673288447781f705a078472cf8808e05a36f129c81c8e20"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "poethepoet" +version = "0.23.0" +description = "A task runner that works well with poetry." +optional = false +python-versions = ">=3.8" +files = [ + {file = "poethepoet-0.23.0-py3-none-any.whl", hash = "sha256:d573ff31d7678e62b6f9bc9a1291ae2009ac14e0eead0a450598f9f05abb27a3"}, + {file = "poethepoet-0.23.0.tar.gz", hash = "sha256:62a0a6a518df5985c191aee0c1fcd2bb6a0a04eb102997786fcdf118e4147d22"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +tomli = ">=1.2.2" + +[package.extras] +poetry-plugin = ["poetry (>=1.0,<2.0)"] + +[[package]] +name = "pybind11-stubgen" +version = "2.5.1" +description = "PEP 561 type stubs generator for pybind11 modules" +optional = false +python-versions = "~=3.7" +files = [ + {file = "pybind11-stubgen-2.5.1.tar.gz", hash = "sha256:4427a67038a00c5ac1637ffa6c65728c67c5b1251ecc23c7704152be0b14cc0b"}, + {file = "pybind11_stubgen-2.5.1-py3-none-any.whl", hash = "sha256:544d49df57da827c8761e7f6ef6bca996df80a33c9fd21c2521d694d4e72fe8d"}, +] + +[[package]] +name = "pyqt6" +version = "6.7.0" +description = "Python bindings for the Qt cross platform application toolkit" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyQt6-6.7.0-1-cp38-abi3-macosx_10_14_universal2.whl", hash = "sha256:656734112853fde1be0963f0ad362e5efd87ba6c6ff234cb1f9fe8003ee254e6"}, + {file = "PyQt6-6.7.0-1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fa2d27fc2f5340f3f1e145c815101ef4550771a9e4bfafd4c7c2479fe83d9488"}, + {file = "PyQt6-6.7.0-1-cp38-abi3-win_amd64.whl", hash = "sha256:6a1f6dfe03752f888b5e628c208f9fd1a03bda7ebda59ffed8c13580289a1892"}, + {file = "PyQt6-6.7.0-cp38-abi3-macosx_10_14_universal2.whl", hash = "sha256:919ffb01020ece42209228bf94b4f2c156a6b77cc5a69a90a05e358b0333750b"}, + {file = "PyQt6-6.7.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e294f025f94493ee12b66efd6893fab309c9063172bb8a5b184f84dfc1ebcc49"}, + {file = "PyQt6-6.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:9d8865fb6357dba032002c4554a9648e88f2b4706c929cc51fba58edafad91fc"}, + {file = "PyQt6-6.7.0.tar.gz", hash = "sha256:3d31b2c59dc378ee26e16586d9469842483588142fc377280aad22aaf2fa6235"}, +] + +[package.dependencies] +PyQt6-Qt6 = ">=6.7.0,<6.8.0" +PyQt6-sip = ">=13.6,<14" + +[[package]] +name = "pyqt6-qt6" +version = "6.7.2" +description = "The subset of a Qt installation needed by PyQt6." +optional = false +python-versions = "*" +files = [ + {file = "PyQt6_Qt6-6.7.2-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:065415589219a2f364aba29d6a98920bb32810286301acbfa157e522d30369e3"}, + {file = "PyQt6_Qt6-6.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f817efa86a0e8eda9152c85b73405463fbf3266299090f32bbb2266da540ead"}, + {file = "PyQt6_Qt6-6.7.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:fc93945eaef4536d68bd53566535efcbe78a7c05c2a533790a8fd022bac8bfaa"}, + {file = "PyQt6_Qt6-6.7.2-py3-none-win_amd64.whl", hash = "sha256:b2d7e5ddb1b9764cd60f1d730fa7bf7a1f0f61b2630967c81761d3d0a5a8a2e0"}, +] + +[[package]] +name = "pyqt6-sip" +version = "13.6.0" +description = "The sip module support for PyQt6" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyQt6_sip-13.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d6b5f699aaed0ac1fcd23e8fbca70d8a77965831b7c1ce474b81b1678817a49d"}, + {file = "PyQt6_sip-13.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8c282062125eea5baf830c6998587d98c50be7c3a817a057fb95fef647184012"}, + {file = "PyQt6_sip-13.6.0-cp310-cp310-win32.whl", hash = "sha256:fa759b6339ff7e25f9afe2a6b651b775f0a36bcb3f5fa85e81a90d3b033c83f4"}, + {file = "PyQt6_sip-13.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:8f9df9f7ccd8a9f0f1d36948c686f03ce1a1281543a3e636b7b7d5e086e1a436"}, + {file = "PyQt6_sip-13.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b9c6b6f9cfccb48cbb78a59603145a698fb4ffd176764d7083e5bf47631d8df"}, + {file = "PyQt6_sip-13.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:86a7b67c64436e32bffa9c28c9f21bf14a9faa54991520b12c3f6f435f24df7f"}, + {file = "PyQt6_sip-13.6.0-cp311-cp311-win32.whl", hash = "sha256:58f68a48400e0b3d1ccb18090090299bad26e3aed7ccb7057c65887b79b8aeea"}, + {file = "PyQt6_sip-13.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:0dfd22cfedd87e96f9d51e0778ca2ba3dc0be83e424e9e0f98f6994d8d9c90f0"}, + {file = "PyQt6_sip-13.6.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3bf03e130fbfd75c9c06e687b86ba375410c7a9e835e4e03285889e61dd4b0c4"}, + {file = "PyQt6_sip-13.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:43fb8551796030aae3d66d6e35e277494071ec6172cd182c9569ab7db268a2f5"}, + {file = "PyQt6_sip-13.6.0-cp312-cp312-win32.whl", hash = "sha256:13885361ca2cb2f5085d50359ba61b3fabd41b139fb58f37332acbe631ef2357"}, + {file = "PyQt6_sip-13.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:24441032a29791e82beb7dfd76878339058def0e97fdb7c1cea517f3a0e6e96b"}, + {file = "PyQt6_sip-13.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3075d8b325382750829e6cde6971c943352309d35768a4d4da0587459606d562"}, + {file = "PyQt6_sip-13.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a6ce80bc24618d8a41be8ca51ad9f10e8bc4296dd90ab2809573df30a23ae0e5"}, + {file = "PyQt6_sip-13.6.0-cp38-cp38-win32.whl", hash = "sha256:fa7b10af7488efc5e53b41dd42c0f421bde6c2865a107af7ae259aff9d841da9"}, + {file = "PyQt6_sip-13.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:9adf672f9114687533a74d5c2d4c03a9a929ad5ad9c3e88098a7da1a440ab916"}, + {file = "PyQt6_sip-13.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98bf954103b087162fa63b3a78f30b0b63da22fd6450b610ec1b851dbb798228"}, + {file = "PyQt6_sip-13.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:39854dba35f8e5a4288da26ecb5f40b4c5ec1932efffb3f49d5ea435a7f37fb3"}, + {file = "PyQt6_sip-13.6.0-cp39-cp39-win32.whl", hash = "sha256:747f6ca44af81777a2c696bd501bc4815a53ec6fc94d4e25830e10bc1391f8ab"}, + {file = "PyQt6_sip-13.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:33ea771fe777eb0d1a2c3ef35bcc3f7a286eb3ff09cd5b2fdd3d87d1f392d7e8"}, + {file = "PyQt6_sip-13.6.0.tar.gz", hash = "sha256:2486e1588071943d4f6657ba09096dc9fffd2322ad2c30041e78ea3f037b5778"}, +] + +[[package]] +name = "pyright" +version = "1.1.369" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.369-py3-none-any.whl", hash = "sha256:06d5167a8d7be62523ced0265c5d2f1e022e110caf57a25d92f50fb2d07bcda0"}, + {file = "pyright-1.1.369.tar.gz", hash = "sha256:ad290710072d021e213b98cc7a2f90ae3a48609ef5b978f749346d1a47eb9af8"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[package]] +name = "pytest" +version = "8.2.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2.0" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "ruff" +version = "0.2.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, + {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, + {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, + {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, + {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "4a3746f1080ab40c4cfde6777517db502e124fe088e971895bd7f7c245c3fa3e" diff --git a/libs/plugin_python/pyproject.toml b/libs/plugin_python/pyproject.toml new file mode 100644 index 0000000..71bb9df --- /dev/null +++ b/libs/plugin_python/pyproject.toml @@ -0,0 +1,53 @@ +[tool.poetry] +name = "modorganizer-plugin_python" +version = "3.0.0" +description = "" +authors = ["Mikaël Capelle "] + +[tool.poetry.dependencies] +python = "^3.12" + +[tool.poetry.group.dev.dependencies] +pyright = "^1.1.369" +ruff = "^0.2.1" +poethepoet = "^0.23.0" +mobase-stubs = { version = "^2.5.1a0", allow-prereleases = true } +pyqt6 = "^6.7.0" +pytest = "^8.2.2" +pybind11-stubgen = "^2.5.1" + +[tool.poe.tasks] +format-imports = "ruff check --select I tests typings --fix" +format-ruff = "ruff format tests typings" +format.sequence = ["format-imports", "format-ruff"] +lint-ruff = "ruff check tests typings" +lint-ruff-format = "ruff format --check tests typings" +lint.sequence = ["lint-ruff", "lint-ruff-format"] +lint.ignore_fail = "return_non_zero" + +[tool.ruff] +target-version = "py312" + +[tool.ruff.lint] +extend-select = ["B", "Q", "I"] + +[tool.ruff.lint.isort.sections] +mobase = ["mobase"] +mobase_tests = ["mobase_tests"] + +[tool.ruff.lint.isort] +section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "mobase", + "mobase_tests", + "local-folder", +] + +[tool.pyright] +typeCheckingMode = "strict" +reportMissingTypeStubs = true +reportMissingModuleSource = false +pythonPlatform = "Windows" diff --git a/libs/plugin_python/src/CMakeLists.txt b/libs/plugin_python/src/CMakeLists.txt new file mode 100644 index 0000000..533a5ab --- /dev/null +++ b/libs/plugin_python/src/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) + +# order matters +add_subdirectory(pybind11-qt) +add_subdirectory(pybind11-utils) +add_subdirectory(mobase) +add_subdirectory(runner) +add_subdirectory(proxy) diff --git a/libs/plugin_python/src/mobase/CMakeLists.txt b/libs/plugin_python/src/mobase/CMakeLists.txt new file mode 100644 index 0000000..2fac52f --- /dev/null +++ b/libs/plugin_python/src/mobase/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Qt6 COMPONENTS Core) +if(NOT TARGET mo2::uibase) + find_package(mo2-uibase CONFIG REQUIRED) +endif() + +pybind11_add_module(mobase MODULE) +mo2_default_source_group() +mo2_configure_target(mobase + NO_SOURCES + WARNINGS 4 + EXTERNAL_WARNINGS 4 + AUTOMOC ON + TRANSLATIONS OFF +) +mo2_target_sources(mobase + FOLDER src + PRIVATE + deprecation.cpp + deprecation.h + mobase.cpp + pybind11_all.h +) +mo2_target_sources(mobase + FOLDER src/wrappers + PRIVATE + ./wrappers/basic_classes.cpp + ./wrappers/game_features.cpp + ./wrappers/known_folders.h + ./wrappers/pyfiletree.cpp + ./wrappers/pyfiletree.h + ./wrappers/pyplugins.cpp + ./wrappers/pyplugins.h + ./wrappers/utils.cpp + ./wrappers/widgets.cpp + ./wrappers/wrappers.cpp + ./wrappers/wrappers.h +) +target_link_libraries(mobase PRIVATE pybind11::qt pybind11::utils mo2::uibase Qt6::Core) diff --git a/libs/plugin_python/src/mobase/README.md b/libs/plugin_python/src/mobase/README.md new file mode 100644 index 0000000..b4d184e --- /dev/null +++ b/libs/plugin_python/src/mobase/README.md @@ -0,0 +1,110 @@ +# mobase + +`mobase` is a ModOrganizer2 Python API. +It provides access to the part of +[`uibase`](https://github.com/ModOrganizer2/modorganizer-uibase) C++ API from +Python through [`pybind11`](https://github.com/pybind/pybind11). + +## Organization + +**Important:** All (most) files should include `pybind11_all.h` (either directly +or through another header) to get proper `type_caster` available. + +- `mobase.cpp` contains the `PYBIND11_MODULE` definition of `mobase` but is otherwise + the entrypoint for other functions. +- `wrappers.h` contains the declaration of most functions implemented under + `wrappers/`. +- The other files under `wrappers/` contains bindings and trampoline classes (see + below) for `uibase` classes. + - `basic_classes.cpp` contains the bindings for most classes that cannot be extended + in Python (`IOrganizer`, `IModInterface`, etc.) + - `game_features.cpp` contains the bindings and trampoline classes for game features. + - `pyfiletree.h` and `pyfiletree.cpp` contains bindings for the `IFileTree`-related + classes. + - `pyplugins.h` contains the trampoline classes for the `IPluginXXX` classes and + `pyplugins.cpp` the bindings. + - `pyplugins.h` is required since the trampoline classes are tagged with `Q_OBJECT`, + and MOC does not work if the classes are declared in a C++ file. + - `pyplugins.cpp` also contains the `extract_plugins` function in `mobase.private` + that is used to extract plugins from Python object in the runner. + - `widgets.cpp` contains the bindings for the widget classes. + - `wrappers.cpp` contains the trampoline and bindings for non-plugin classes that can + be extended through Python. + +## Updating mobase + +### Classes that cannot be extended through Python + +Updating or adding classes that cannot be extended through Python is quite easily. +One simply needs to declare the appropriate `py::class_` or add new `.def()`. + +See below for things to remember when creating pybind11 bindings. + +### Free functions + +Similar to classes that cannot be extended through Python, see above. + +### Classes that can be extended through Python: Plugins + +To extend plugins, simply update the trampoline classes in `pyplugins.h` and the +bindings in `pyplugins.cpp`. + +**Note:** For new plugins, simply look at the existing one. + +### Classes that can be extended through Python: Game Features + +To extend or expose game features: + +- Create (if there is not already one) a trampoline class for the feature in + `game_features.cpp`. + - Add implementation of missing functions if required. +- Add the bindings in `add_game_feature_bindings` in `game_features.cpp`. +- For new feature, add the feature type to `GameFeaturesHelper::GameFeatures` in + `game_features.cpp`. + +### Classes that can be extended through Python: Others + +Non-plugin classes should be added to the `wrappers.cpp` file and should be exposed +with `std::shared_ptr<>` or `qobject_holder<>` holders. + +- If the classes extends `QObject`, use a `qobject_holder`. +- Otherwise use a `std::shared_ptr<>` holder and add a `MO2_PYBIND11_SHARED_CPP_HOLDER` + declaration in `pybind11_all.h`. + +Trampoline can be defined directly in `wrappers.cpp`, and bindings in the appropriate +function. +See the existing classes for example. + +**Important:** +You need to make sure that `uibase` manipulates such classes through +`std::shared_ptr<>` (unless those inherit `QObject`). +Using `std::unique_ptr<>` is not possible since `std::unique_ptr<>` cannot have custom +runtime-specified deleters. + +## Things to remember + +Here are a few things to remember when creating bindings: + +- If a function has multiple overloads that can conflict in Python, the more complex + one must be defined first as pybind11 will try calling them in order. +- If a C++ function expect a `QString`, `QFileInfo` or `QDir` that represents a file or + a directory, wrapping the function with `wrap_for_filepath` or `wrap_for_directory` is + a good idea. This allows Python to call the function with `pathlib.Path`. +- Most of the C++ function taking a reference to modify in C++ cannot be directly + exposed in Python since Python cannot modify reference to simple type (e.g. + `QString&` or `int&`). + The best way to expose such function is to bind a lambda that returns a variant from + Python, e.g. + +```cpp +// assume the C++ function is QString fn(QString& foo, QString const& bar, int& maz); + +m.def("function", [](QString& foo, QString const& bar, int& maz) { + // call the function + auto ret = function(foo, bar, maz); + + // make a tuple containing the return value (if there is one), and the modified + // values passed by reference + return std::make_tuple(ret, foo, maz); +}); +``` diff --git a/libs/plugin_python/src/mobase/deprecation.cpp b/libs/plugin_python/src/mobase/deprecation.cpp new file mode 100644 index 0000000..958712d --- /dev/null +++ b/libs/plugin_python/src/mobase/deprecation.cpp @@ -0,0 +1,55 @@ +#include "deprecation.h" + +#include +#include + +#include + +#include + +#include + +namespace py = pybind11; + +namespace mo2::python { + + void show_deprecation_warning(std::string_view name, std::string_view message, + bool show_once) + { + + // Contains the list of filename / line number for which a deprecation + // warning has already been shown. + static std::set> DeprecatedLines; + + // Find the caller: + auto inspect = py::module_::import("inspect"); + auto current_frame = inspect.attr("currentframe")(); + py::sequence callable_frame = inspect.attr("getouterframes")(current_frame, 2); + auto last_frame = callable_frame[py::int_(-1)]; + auto filename = last_frame.attr("filename").cast(); + auto function = last_frame.attr("function").cast(); + auto lineno = last_frame.attr("lineno").cast(); + + // Only show once if requested: + if (show_once && DeprecatedLines.contains({filename, lineno})) { + return; + } + + // Register the deprecation: + DeprecatedLines.emplace(filename, lineno); + + auto path = relative(std::filesystem::path(filename), + QCoreApplication::applicationDirPath().toStdWString()); + + // Show the message: + if (message.empty()) { + MOBase::log::warn("[deprecated] {} in {} [{}:{}].", name, function, + path.native(), lineno); + } + else { + MOBase::log::warn("[deprecated] {} in {} [{}:{}]: {}", name, function, + path.native(), lineno, message); + } + } + +} // namespace mo2::python diff --git a/libs/plugin_python/src/mobase/deprecation.h b/libs/plugin_python/src/mobase/deprecation.h new file mode 100644 index 0000000..d9a445c --- /dev/null +++ b/libs/plugin_python/src/mobase/deprecation.h @@ -0,0 +1,27 @@ +#ifndef PYTHONRUNNER_UTILS_H +#define PYTHONRUNNER_UTILS_H + +#include + +#include + +namespace mo2::python { + + /** + * @brief Show a deprecation warning. + * + * This methods will print a warning in MO2 log containing the location of + * the call to the deprecated function. If show_once is true, the + * deprecation warning will only be logged the first time the function is + * called at this location. + * + * @param name Name of the deprecated function. + * @param message Deprecation message. + * @param show_once Only show the message once per call location. + */ + void show_deprecation_warning(std::string_view name, std::string_view message = "", + bool show_once = true); + +} // namespace mo2::python + +#endif diff --git a/libs/plugin_python/src/mobase/mobase.cpp b/libs/plugin_python/src/mobase/mobase.cpp new file mode 100644 index 0000000..b1daa2b --- /dev/null +++ b/libs/plugin_python/src/mobase/mobase.cpp @@ -0,0 +1,86 @@ +#pragma warning(disable : 4100) +#pragma warning(disable : 4996) + +#include +#include +#include + +#include + +#include "pybind11_all.h" + +#include "wrappers/pyfiletree.h" +#include "wrappers/wrappers.h" + +using namespace MOBase; +namespace py = pybind11; + +PYBIND11_MODULE(mobase, m) +{ + using namespace mo2::python; + + m.add_object("PyQt6", py::module_::import("PyQt6")); + m.add_object("PyQt6.QtCore", py::module_::import("PyQt6.QtCore")); + m.add_object("PyQt6.QtGui", py::module_::import("PyQt6.QtGui")); + m.add_object("PyQt6.QtWidgets", py::module_::import("PyQt6.QtWidgets")); + + // bindings + // + mo2::python::add_basic_bindings(m); + mo2::python::add_wrapper_bindings(m); + + // game features must be added before plugins + mo2::python::add_game_feature_bindings(m); + mo2::python::add_igamefeatures_classes(m); + + mo2::python::add_plugins_bindings(m); + + // widgets + // + py::module_ widgets = m.def_submodule("widgets"); + mo2::python::add_widget_bindings(widgets); + + // utils + // + py::module_ utils = m.def_submodule("utils"); + mo2::python::add_utils_bindings(utils); + + // functions + // + m.def("getFileVersion", wrap_for_filepath(&MOBase::getFileVersion), + py::arg("filepath")); + m.def("getProductVersion", wrap_for_filepath(&MOBase::getProductVersion), + py::arg("executable")); + m.def("getIconForExecutable", wrap_for_filepath(&MOBase::iconForExecutable), + py::arg("executable")); + + // typing stuff to be consistent with stubs and allow plugin developers to properly + // type their code if they want + { + m.add_object("TypeVar", py::module_::import("typing").attr("TypeVar")); + + auto s = m.attr("__dict__"); + + // expose MoVariant + // + // this needs to be defined, otherwise MoVariant is not found when actually + // running plugins through MO2, making them crash (if plugins use MoVariant in + // their own code) + // + m.add_object( + "MoVariant", + py::eval("None | bool | int | str | list[object] | dict[str, object]")); + + // same thing for GameFeatureType + // + m.add_object("GameFeatureType", py::eval("TypeVar('GameFeatureType')", s)); + } + + // private stuff for debugging/test + py::module_ moprivate = m.def_submodule("private"); + + // expose a function to create a particular tree, only for debugging + // purpose, not in mobase. + mo2::python::add_make_tree_function(moprivate); + moprivate.def("extract_plugins", &mo2::python::extract_plugins); +} diff --git a/libs/plugin_python/src/mobase/pybind11_all.h b/libs/plugin_python/src/mobase/pybind11_all.h new file mode 100644 index 0000000..c92d7ff --- /dev/null +++ b/libs/plugin_python/src/mobase/pybind11_all.h @@ -0,0 +1,146 @@ +#ifndef PYTHON_PYBIND11_ALL_H +#define PYTHON_PYBIND11_ALL_H + +#include + +#include + +#include +#include +#include +#include + +#include "pybind11_qt/pybind11_qt.h" + +#include "pybind11_utils/functional.h" +#include "pybind11_utils/generator.h" +#include "pybind11_utils/shared_cpp_owner.h" +#include "pybind11_utils/smart_variant_wrapper.h" + +#include +#include +#include + +namespace mo2::python { + + namespace detail { + + template <> + struct smart_variant_converter { + + static QString from(std::filesystem::path const& path) + { + return QString::fromStdString(path.string()); + } + + static QString from(QFileInfo const& fileInfo) + { + return fileInfo.filePath(); + } + + static QString from(QDir const& dir) { return dir.path(); } + }; + + template <> + struct smart_variant_converter { + + static std::filesystem::path from(QString const& value) + { + return {value.toStdWString()}; + } + + static std::filesystem::path from(QFileInfo const& fileInfo) + { + return fileInfo.filesystemFilePath(); + } + + static std::filesystem::path from(QDir const& dir) + { + return dir.filesystemPath(); + } + }; + + // we do not need specialization for QFileInfo and QDir because both of them can + // be constructed from std::filesystem::path and QString already + + } // namespace detail + + using FileWrapper = smart_variant; + using DirectoryWrapper = smart_variant; + + // wrap the given function to accept FileWrapper (str | PathLike | QFileInfo) at the + // given argument positions (or any valid positions if Is... is empty) + // + template + auto wrap_for_filepath(Fn&& fn) + { + return mo2::python::wrap_arguments(std::forward(fn)); + } + + // wrap the given function to accept DirectoryWrapper (str | PathLike | QDir) + // at the given argument positions (or any valid positions if Is... is empty) + // + template + auto wrap_for_directory(Fn&& fn) + { + return mo2::python::wrap_arguments( + std::forward(fn)); + } + + // wrap a function-like object to return a FileWrapper instead of its return type, + // useful to generate proper typing in Python + // + // note that QFileInfo has a __fspath__ in Python, so it is quite easy to convert + // from "FileWrapper", a.k.a., str | os.PathLike | QFileInfo to Path by simply + // calling Path() on the return type if necessary + // + // this should be combined with custom return-value in PYBIND11_OVERRIDE(_PURE), see + // ISaveGame binding for an example + // + template + auto wrap_return_for_filepath(Fn&& fn) + { + return mo2::python::wrap_return(std::forward(fn)); + } + + // similar to wrap_return_for_filepath, except it returns a DirectoryWrapper instead + // of its return type + // + // this is much less practical than wrap_return_for_filepath since QDir does not + // expose __fspath__, so more complex things need to be done in Python, which is why + // this should be used carefully (e.g., should not be used if the return type is + // already QDir) + // + template + auto wrap_return_for_directory(Fn&& fn) + { + return mo2::python::wrap_return(std::forward(fn)); + } + + // convert a QList to QStringList - QString must be constructible from QString + // + template + QStringList toQStringList(QList const& list) + { + static_assert(std::is_constructible_v, + "QString must be constructible from T."); + return {list.begin(), list.end()}; + } + + // convert a QStringList to a QList - T must be constructible from QString + // + template + QList toQList(QStringList const& list) + { + static_assert(std::is_constructible_v, + "T must be constructible from QString."); + return {list.begin(), list.end()}; + } + +} // namespace mo2::python + +MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::IPluginRequirement) +MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::ISaveGame) +MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::GameFeature) + +#endif diff --git a/libs/plugin_python/src/mobase/wrappers/basic_classes.cpp b/libs/plugin_python/src/mobase/wrappers/basic_classes.cpp new file mode 100644 index 0000000..ea69a46 --- /dev/null +++ b/libs/plugin_python/src/mobase/wrappers/basic_classes.cpp @@ -0,0 +1,952 @@ +#include "wrappers.h" + +#include "../pybind11_all.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../deprecation.h" +#include "pyfiletree.h" + +using namespace MOBase; + +namespace mo2::python { + + namespace py = pybind11; + + using namespace pybind11::literals; + + void add_versioninfo_classes(py::module_ m) + { + // Version + py::class_ pyVersion(m, "Version"); + + py::enum_(pyVersion, "ReleaseType") + .value("DEVELOPMENT", Version::Development) + .value("ALPHA", Version::Alpha) + .value("BETA", Version::Beta) + .value("RELEASE_CANDIDATE", Version::ReleaseCandidate) + .export_values(); + + py::enum_(pyVersion, "ParseMode") + .value("SEMVER", Version::ParseMode::SemVer) + .value("MO2", Version::ParseMode::MO2); + + py::enum_(pyVersion, "FormatMode", py::arithmetic{}) + .value("FORCE_SUBPATCH", Version::FormatMode::ForceSubPatch) + .value("NO_SEPARATOR", Version::FormatMode::NoSeparator) + .value("SHORT_ALPHA_BETA", Version::FormatMode::ShortAlphaBeta) + .value("NO_METADATA", Version::FormatMode::NoMetadata) + .value("CONDENSED", + static_cast(Version::FormatCondensed.toInt())) + .export_values() + .def("__xor__", + py::overload_cast( + &operator^)) + .def("__and__", + py::overload_cast( + &operator&)) + .def("__or__", py::overload_cast( + &operator|)) + .def("__rxor__", + py::overload_cast( + &operator^)) + .def("__rand__", + py::overload_cast( + &operator&)) + .def("__ror__", + py::overload_cast( + &operator|)); + + pyVersion + .def_static("parse", &Version::parse, "value"_a, + "mode"_a = Version::ParseMode::SemVer) + .def(py::init(), "major"_a, "minor"_a, "patch"_a, + "metadata"_a = "") + .def(py::init(), "major"_a, "minor"_a, + "patch"_a, "subpatch"_a, "metadata"_a = "") + .def(py::init(), "major"_a, + "minor"_a, "patch"_a, "type"_a, "metadata"_a = "") + .def(py::init(), + "major"_a, "minor"_a, "patch"_a, "subpatch"_a, "type"_a, + "metadata"_a = "") + .def(py::init(), + "major"_a, "minor"_a, "patch"_a, "type"_a, "prerelease"_a, + "metadata"_a = "") + .def(py::init(), + "major"_a, "minor"_a, "patch"_a, "subpatch"_a, "type"_a, + "prerelease"_a, "metadata"_a = "") + .def(py::init>, + QString>(), + "major"_a, "minor"_a, "patch"_a, "subpatch"_a, "prereleases"_a, + "metadata"_a = "") + .def("isPreRelease", &Version::isPreRelease) + .def_property_readonly("major", &Version::major) + .def_property_readonly("minor", &Version::minor) + .def_property_readonly("patch", &Version::patch) + .def_property_readonly("subpatch", &Version::subpatch) + .def_property_readonly("prereleases", &Version::preReleases) + .def_property_readonly("build_metadata", &Version::buildMetadata) + .def("string", &Version::string, "mode"_a = Version::FormatModes{}) + .def("__str__", + [](Version const& version) { + return version.string(Version::FormatCondensed); + }) + .def(py::self < py::self) + .def(py::self > py::self) + .def(py::self <= py::self) + .def(py::self >= py::self) + .def(py::self != py::self) + .def(py::self == py::self); + + // VersionInfo + py::enum_(m, "ReleaseType") + .value("final", MOBase::VersionInfo::RELEASE_FINAL) + .value("candidate", MOBase::VersionInfo::RELEASE_CANDIDATE) + .value("beta", MOBase::VersionInfo::RELEASE_BETA) + .value("alpha", MOBase::VersionInfo::RELEASE_ALPHA) + .value("prealpha", MOBase::VersionInfo::RELEASE_PREALPHA) + + .value("FINAL", MOBase::VersionInfo::RELEASE_FINAL) + .value("CANDIDATE", MOBase::VersionInfo::RELEASE_CANDIDATE) + .value("BETA", MOBase::VersionInfo::RELEASE_BETA) + .value("ALPHA", MOBase::VersionInfo::RELEASE_ALPHA) + .value("PRE_ALPHA", MOBase::VersionInfo::RELEASE_PREALPHA); + + py::enum_(m, "VersionScheme") + .value("discover", MOBase::VersionInfo::SCHEME_DISCOVER) + .value("regular", MOBase::VersionInfo::SCHEME_REGULAR) + .value("decimalmark", MOBase::VersionInfo::SCHEME_DECIMALMARK) + .value("numbersandletters", MOBase::VersionInfo::SCHEME_NUMBERSANDLETTERS) + .value("date", MOBase::VersionInfo::SCHEME_DATE) + .value("literal", MOBase::VersionInfo::SCHEME_LITERAL) + + .value("DISCOVER", MOBase::VersionInfo::SCHEME_DISCOVER) + .value("REGULAR", MOBase::VersionInfo::SCHEME_REGULAR) + .value("DECIMAL_MARK", MOBase::VersionInfo::SCHEME_DECIMALMARK) + .value("NUMBERS_AND_LETTERS", MOBase::VersionInfo::SCHEME_NUMBERSANDLETTERS) + .value("DATE", MOBase::VersionInfo::SCHEME_DATE) + .value("LITERAL", MOBase::VersionInfo::SCHEME_LITERAL); + + py::class_(m, "VersionInfo") + .def(py::init<>()) + .def(py::init(), "value"_a, + "scheme"_a = VersionInfo::SCHEME_DISCOVER) + // note: order of the two init<> below is important because + // ReleaseType is a simple enum with an implicit int conversion. + .def(py::init(), "major"_a, + "minor"_a, "subminor"_a, "subsubminor"_a, + "release_type"_a = VersionInfo::RELEASE_FINAL) + .def(py::init(), "major"_a, + "minor"_a, "subminor"_a, "release_type"_a = VersionInfo::RELEASE_FINAL) + .def("clear", &VersionInfo::clear) + .def("parse", &VersionInfo::parse, "value"_a, + "scheme"_a = VersionInfo::SCHEME_DISCOVER, "is_manual"_a = false) + .def("canonicalString", &VersionInfo::canonicalString) + .def("displayString", &VersionInfo::displayString, "forced_segments"_a = 2) + .def("isValid", &VersionInfo::isValid) + .def("scheme", &VersionInfo::scheme) + .def("__str__", &VersionInfo::canonicalString) + .def(py::self < py::self) + .def(py::self > py::self) + .def(py::self <= py::self) + .def(py::self >= py::self) + .def(py::self != py::self) + .def(py::self == py::self); + } + + void add_executable_classes(py::module_ m) + { + py::class_(m, "ExecutableInfo") + .def(py::init(), "title"_a, "binary"_a) + .def("withArgument", &ExecutableInfo::withArgument, "argument"_a) + .def("withWorkingDirectory", + wrap_for_directory(&ExecutableInfo::withWorkingDirectory), + "directory"_a) + .def("withSteamAppId", &ExecutableInfo::withSteamAppId, "app_id"_a) + .def("asCustom", &ExecutableInfo::asCustom) + .def("isValid", &ExecutableInfo::isValid) + .def("title", &ExecutableInfo::title) + .def("binary", &ExecutableInfo::binary) + .def("arguments", &ExecutableInfo::arguments) + .def("workingDirectory", &ExecutableInfo::workingDirectory) + .def("steamAppID", &ExecutableInfo::steamAppID) + .def("isCustom", &ExecutableInfo::isCustom); + + py::class_(m, "ExecutableForcedLoadSetting") + .def(py::init(), "process"_a, "library"_a) + .def("withForced", &ExecutableForcedLoadSetting::withForced, "forced"_a) + .def("withEnabled", &ExecutableForcedLoadSetting::withEnabled, "enabled"_a) + .def("enabled", &ExecutableForcedLoadSetting::enabled) + .def("forced", &ExecutableForcedLoadSetting::forced) + .def("library", &ExecutableForcedLoadSetting::library) + .def("process", &ExecutableForcedLoadSetting::process); + + py::class_(m, "IExecutable") + .def("title", &IExecutable::title) + .def("binaryInfo", &IExecutable::binaryInfo) + .def("arguments", &IExecutable::arguments) + .def("steamAppID", &IExecutable::steamAppID) + .def("workingDirectory", &IExecutable::workingDirectory) + .def("isShownOnToolbar", &IExecutable::isShownOnToolbar) + .def("usesOwnIcon", &IExecutable::usesOwnIcon) + .def("minimizeToSystemTray", &IExecutable::minimizeToSystemTray) + .def("hide", &IExecutable::hide); + + py::class_(m, "IExecutablesList") + .def("executables", + [](IExecutablesList* executablesList) { + return make_generator(executablesList->executables(), + py::return_value_policy::reference); + }) + .def("getByTitle", &IExecutablesList::getByTitle, "title"_a) + .def("getByBinary", &IExecutablesList::getByBinary, "info"_a) + .def("titleExists", &IExecutablesList::contains, "title"_a); + } + + void add_modinterface_classes(py::module_ m) + { + py::enum_(m, "EndorsedState", py::arithmetic()) + .value("ENDORSED_FALSE", EndorsedState::ENDORSED_FALSE) + .value("ENDORSED_TRUE", EndorsedState::ENDORSED_TRUE) + .value("ENDORSED_UNKNOWN", EndorsedState::ENDORSED_UNKNOWN) + .value("ENDORSED_NEVER", EndorsedState::ENDORSED_NEVER); + + py::enum_(m, "TrackedState", py::arithmetic()) + .value("TRACKED_FALSE", TrackedState::TRACKED_FALSE) + .value("TRACKED_TRUE", TrackedState::TRACKED_TRUE) + .value("TRACKED_UNKNOWN", TrackedState::TRACKED_UNKNOWN); + + py::class_(m, "IModInterface") + .def("name", &IModInterface::name) + .def("absolutePath", &IModInterface::absolutePath) + + .def("comments", &IModInterface::comments) + .def("notes", &IModInterface::notes) + .def("gameName", &IModInterface::gameName) + .def("repository", &IModInterface::repository) + .def("nexusId", &IModInterface::nexusId) + .def("version", &IModInterface::version) + .def("newestVersion", &IModInterface::newestVersion) + .def("ignoredVersion", &IModInterface::ignoredVersion) + .def("installationFile", &IModInterface::installationFile) + .def("converted", &IModInterface::converted) + .def("validated", &IModInterface::validated) + .def("color", &IModInterface::color) + .def("url", &IModInterface::url) + .def("primaryCategory", &IModInterface::primaryCategory) + .def("categories", &IModInterface::categories) + .def("author", &IModInterface::author) + .def("uploader", &IModInterface::uploader) + .def("uploaderUrl", &IModInterface::uploaderUrl) + .def("trackedState", &IModInterface::trackedState) + .def("endorsedState", &IModInterface::endorsedState) + .def("fileTree", &IModInterface::fileTree) + .def("isOverwrite", &IModInterface::isOverwrite) + .def("isBackup", &IModInterface::isBackup) + .def("isSeparator", &IModInterface::isSeparator) + .def("isForeign", &IModInterface::isForeign) + + .def("setVersion", &IModInterface::setVersion, "version"_a) + .def("setNewestVersion", &IModInterface::setNewestVersion, "version"_a) + .def("setIsEndorsed", &IModInterface::setIsEndorsed, "endorsed"_a) + .def("setNexusID", &IModInterface::setNexusID, "nexus_id"_a) + .def("addNexusCategory", &IModInterface::addNexusCategory, "category_id"_a) + .def("addCategory", &IModInterface::addCategory, "name"_a) + .def("removeCategory", &IModInterface::removeCategory, "name"_a) + .def("setGameName", &IModInterface::setGameName, "name"_a) + .def("setUrl", &IModInterface::setUrl, "url"_a) + .def("pluginSetting", &IModInterface::pluginSetting, "plugin_name"_a, + "key"_a, "default"_a = QVariant()) + .def("pluginSettings", &IModInterface::pluginSettings, "plugin_name"_a) + .def("setPluginSetting", &IModInterface::setPluginSetting, "plugin_name"_a, + "key"_a, "value"_a) + .def("clearPluginSettings", &IModInterface::clearPluginSettings, + "plugin_name"_a); + } + + void add_modrepository_classes(py::module_ m) + { + py::class_ iModRepositoryBridge(m, + "IModRepositoryBridge"); + iModRepositoryBridge + .def("requestDescription", &IModRepositoryBridge::requestDescription, + "game_name"_a, "mod_id"_a, "user_data"_a) + .def("requestFiles", &IModRepositoryBridge::requestFiles, "game_name"_a, + "mod_id"_a, "user_data"_a) + .def("requestFileInfo", &IModRepositoryBridge::requestFileInfo, + "game_name"_a, "mod_id"_a, "file_id"_a, "user_data"_a) + .def("requestDownloadURL", &IModRepositoryBridge::requestDownloadURL, + "game_name"_a, "mod_id"_a, "file_id"_a, "user_data"_a) + .def("requestToggleEndorsement", + &IModRepositoryBridge::requestToggleEndorsement, "game_name"_a, + "mod_id"_a, "mod_version"_a, "endorse"_a, "user_data"_a); + + py::qt::add_qt_delegate(iModRepositoryBridge, "_object"); + + py::class_(m, "ModRepositoryFileInfo") + .def(py::init(), "other"_a) + .def(py::init(), "game_name"_a = "", "mod_id"_a = 0, + "file_id"_a = 0) + .def("__str__", &ModRepositoryFileInfo::toString) + .def_static("createFromJson", &ModRepositoryFileInfo::createFromJson, + "data"_a) + .def_readwrite("name", &ModRepositoryFileInfo::name) + .def_readwrite("uri", &ModRepositoryFileInfo::uri) + .def_readwrite("description", &ModRepositoryFileInfo::description) + .def_readwrite("version", &ModRepositoryFileInfo::version) + .def_readwrite("newestVersion", &ModRepositoryFileInfo::newestVersion) + .def_readwrite("categoryID", &ModRepositoryFileInfo::categoryID) + .def_readwrite("modName", &ModRepositoryFileInfo::modName) + .def_readwrite("gameName", &ModRepositoryFileInfo::gameName) + .def_readwrite("modID", &ModRepositoryFileInfo::modID) + .def_readwrite("fileID", &ModRepositoryFileInfo::fileID) + .def_readwrite("fileSize", &ModRepositoryFileInfo::fileSize) + .def_readwrite("fileName", &ModRepositoryFileInfo::fileName) + .def_readwrite("fileCategory", &ModRepositoryFileInfo::fileCategory) + .def_readwrite("fileTime", &ModRepositoryFileInfo::fileTime) + .def_readwrite("repository", &ModRepositoryFileInfo::repository) + .def_readwrite("userData", &ModRepositoryFileInfo::userData) + .def_readwrite("author", &ModRepositoryFileInfo::author) + .def_readwrite("uploader", &ModRepositoryFileInfo::uploader) + .def_readwrite("uploaderUrl", &ModRepositoryFileInfo::uploaderUrl); + } + + void add_guessedstring_classes(py::module_ m) + { + py::enum_(m, "GuessQuality") + .value("INVALID", MOBase::GUESS_INVALID) + .value("FALLBACK", MOBase::GUESS_FALLBACK) + .value("GOOD", MOBase::GUESS_GOOD) + .value("META", MOBase::GUESS_META) + .value("PRESET", MOBase::GUESS_PRESET) + .value("USER", MOBase::GUESS_USER); + + py::class_>(m, "GuessedString") + .def(py::init<>()) + .def(py::init(), "value"_a, + "quality"_a = EGuessQuality::GUESS_USER) + .def("update", + py::overload_cast(&GuessedValue::update), + "value"_a) + .def("update", + py::overload_cast( + &GuessedValue::update), + "value"_a, "quality"_a) + + // Methods to simulate the assignment operator: + .def("reset", + [](GuessedValue* gv) { + *gv = GuessedValue(); + return gv; + }) + .def( + "reset", + [](GuessedValue* gv, const QString& value, EGuessQuality eq) { + *gv = GuessedValue(value, eq); + return gv; + }, + "value"_a, "quality"_a) + .def( + "reset", + [](GuessedValue* gv, const GuessedValue& other) { + *gv = other; + return gv; + }, + "other"_a) + + // use an intermediate lambda because we cannot have a function with a + // non-const reference in Python - in Python, the function should returned a + // bool or the modified value + .def( + "setFilter", + [](GuessedValue* gv, + std::function(QString const&)> fn) { + gv->setFilter([fn](QString& s) { + auto ret = fn(s); + return std::visit( + [&s](auto v) { + if constexpr (std::is_same_v) { + s = v; + return true; + } + else if constexpr (std::is_same_v) { + return v; + } + }, + ret); + }); + }, + "filter"_a) + + // this makes a copy in python but it more practical than + // exposing an iterator + .def("variants", &GuessedValue::variants) + .def("__str__", &MOBase::GuessedValue::operator const QString&); + + // implicit conversion from QString - this allows passing Python string to + // function expecting GuessedValue + py::implicitly_convertible>(); + } + + void add_ipluginlist_classes(py::module_ m) + { + py::enum_(m, "PluginState", py::arithmetic()) + .value("missing", IPluginList::STATE_MISSING) + .value("inactive", IPluginList::STATE_INACTIVE) + .value("active", IPluginList::STATE_ACTIVE) + + .value("MISSING", IPluginList::STATE_MISSING) + .value("INACTIVE", IPluginList::STATE_INACTIVE) + .value("ACTIVE", IPluginList::STATE_ACTIVE); + + py::class_(m, "IPluginList") + .def("state", &IPluginList::state, "name"_a) + .def("priority", &IPluginList::priority, "name"_a) + .def("setPriority", &IPluginList::setPriority, "name"_a, "priority"_a) + .def("loadOrder", &IPluginList::loadOrder, "name"_a) + .def("masters", &IPluginList::masters, "name"_a) + .def("origin", &IPluginList::origin, "name"_a) + .def("onRefreshed", &IPluginList::onRefreshed, "callback"_a) + .def("onPluginMoved", &IPluginList::onPluginMoved, "callback"_a) + + .def("isMasterFlagged", &IPluginList::isMasterFlagged, "name"_a) + .def("hasMasterExtension", &IPluginList::hasMasterExtension, "name"_a) + .def("isMediumFlagged", &IPluginList::isMediumFlagged, "name"_a) + .def("isLightFlagged", &IPluginList::isLightFlagged, "name"_a) + .def("isBlueprintFlagged", &IPluginList::isBlueprintFlagged, "name"_a) + .def("hasLightExtension", &IPluginList::hasLightExtension, "name"_a) + .def("hasNoRecords", &IPluginList::hasNoRecords, "name"_a) + + .def("formVersion", &IPluginList::formVersion, "name"_a) + .def("headerVersion", &IPluginList::headerVersion, "name"_a) + .def("author", &IPluginList::author, "name"_a) + .def("description", &IPluginList::description, "name"_a) + + // Kept but deprecated for backward compatibility: + .def( + "onPluginStateChanged", + [](IPluginList* modList, + const std::function& + fn) { + mo2::python::show_deprecation_warning( + "onPluginStateChanged", + "onPluginStateChanged(Callable[[str, " + "IPluginList.PluginStates], None]) is deprecated, " + "use onPluginStateChanged(Callable[[Dict[str, " + "IPluginList.PluginStates], None]) instead."); + return modList->onPluginStateChanged([fn](auto const& map) { + for (const auto& entry : map) { + fn(entry.first, entry.second); + } + }); + }, + "callback"_a) + .def( + "isMaster", + [](IPluginList* modList, QString const& name) { + mo2::python::show_deprecation_warning( + "isMaster", + "isMaster(str) is deprecated, use isMasterFlagged() or " + "hasMasterExtension() instead."); + return modList->isMasterFlagged(name); + }, + "name"_a) + .def("onPluginStateChanged", &MOBase::IPluginList::onPluginStateChanged, + "callback"_a) + .def("pluginNames", &MOBase::IPluginList::pluginNames) + .def("setState", &MOBase::IPluginList::setState, "name"_a, "state"_a) + .def("setLoadOrder", &MOBase::IPluginList::setLoadOrder, "loadorder"_a); + } + + void add_imodlist_classes(py::module_ m) + { + py::enum_(m, "ModState", py::arithmetic()) + .value("exists", IModList::STATE_EXISTS) + .value("active", IModList::STATE_ACTIVE) + .value("essential", IModList::STATE_ESSENTIAL) + .value("empty", IModList::STATE_EMPTY) + .value("endorsed", IModList::STATE_ENDORSED) + .value("valid", IModList::STATE_VALID) + .value("alternate", IModList::STATE_ALTERNATE) + + .value("EXISTS", IModList::STATE_EXISTS) + .value("ACTIVE", IModList::STATE_ACTIVE) + .value("ESSENTIAL", IModList::STATE_ESSENTIAL) + .value("EMPTY", IModList::STATE_EMPTY) + .value("ENDORSED", IModList::STATE_ENDORSED) + .value("VALID", IModList::STATE_VALID) + .value("ALTERNATE", IModList::STATE_ALTERNATE); + + py::class_(m, "IModList") + .def("displayName", &MOBase::IModList::displayName, "name"_a) + .def("allMods", &MOBase::IModList::allMods) + .def("allModsByProfilePriority", + &MOBase::IModList::allModsByProfilePriority, + "profile"_a = static_cast(nullptr)) + .def("getMod", &MOBase::IModList::getMod, + py::return_value_policy::reference, "name"_a) + .def("removeMod", &MOBase::IModList::removeMod, "mod"_a) + .def("renameMod", &MOBase::IModList::renameMod, + py::return_value_policy::reference, "mod"_a, "name"_a) + + .def("state", &MOBase::IModList::state, "name"_a) + .def("setActive", + py::overload_cast( + &MOBase::IModList::setActive), + "names"_a, "active"_a) + .def("setActive", + py::overload_cast(&MOBase::IModList::setActive), + "name"_a, "active"_a) + .def("priority", &MOBase::IModList::priority, "name"_a) + .def("setPriority", &MOBase::IModList::setPriority, "name"_a, "priority"_a) + + // kept but deprecated for backward compatibility + .def( + "onModStateChanged", + [](IModList* modList, + const std::function& fn) { + mo2::python::show_deprecation_warning( + "onModStateChanged", + "onModStateChanged(Callable[[str, IModList.ModStates], None]) " + "is deprecated, " + "use onModStateChanged(Callable[[Dict[str, " + "IModList.ModStates], None]) instead."); + return modList->onModStateChanged([fn](auto const& map) { + for (const auto& entry : map) { + fn(entry.first, entry.second); + } + }); + }, + "callback"_a) + + .def("onModInstalled", &MOBase::IModList::onModInstalled, "callback"_a) + .def("onModRemoved", &MOBase::IModList::onModRemoved, "callback"_a) + .def("onModStateChanged", &MOBase::IModList::onModStateChanged, + "callback"_a) + .def("onModMoved", &MOBase::IModList::onModMoved, "callback"_a); + } + + void add_iorganizer_classes(py::module_ m) + { + // define INVALID_HANDLE_VALUE for startApplication, etc. + m.attr("INVALID_HANDLE_VALUE") = py::int_((std::uintptr_t)INVALID_HANDLE_VALUE); + + py::class_(m, "FileInfo") + .def(py::init<>()) + .def_readwrite("filePath", &IOrganizer::FileInfo::filePath) + .def_readwrite("archive", &IOrganizer::FileInfo::archive) + .def_readwrite("origins", &IOrganizer::FileInfo::origins); + + py::class_(m, "IOrganizer") + .def("createNexusBridge", &IOrganizer::createNexusBridge, + py::return_value_policy::reference) + .def("instanceName", &IOrganizer::instanceName) + .def("profileName", &IOrganizer::profileName) + .def("profilePath", &IOrganizer::profilePath) + .def("downloadsPath", &IOrganizer::downloadsPath) + .def("overwritePath", &IOrganizer::overwritePath) + .def("basePath", &IOrganizer::basePath) + .def("modsPath", &IOrganizer::modsPath) + .def("appVersion", + [](IOrganizer& o) { + mo2::python::show_deprecation_warning( + "appVersion", "IOrganizer::appVersion() is deprecated, use " + "IOrganizer::version() instead."); +#pragma warning(push) +#pragma warning(disable : 4996) + return o.appVersion(); +#pragma warning(pop) + }) + .def("version", &IOrganizer::version) + .def("createMod", &IOrganizer::createMod, + py::return_value_policy::reference, "name"_a) + .def("getGame", &IOrganizer::getGame, py::return_value_policy::reference, + "name"_a) + .def("modDataChanged", &IOrganizer::modDataChanged, "mod"_a) + .def("isPluginEnabled", + py::overload_cast(&IOrganizer::isPluginEnabled, py::const_), + "plugin"_a) + .def("isPluginEnabled", + py::overload_cast(&IOrganizer::isPluginEnabled, + py::const_), + "plugin"_a) + .def("pluginSetting", &IOrganizer::pluginSetting, "plugin_name"_a, "key"_a) + .def("setPluginSetting", &IOrganizer::setPluginSetting, "plugin_name"_a, + "key"_a, "value"_a) + .def("persistent", &IOrganizer::persistent, "plugin_name"_a, "key"_a, + "default"_a = QVariant()) + .def("setPersistent", &IOrganizer::setPersistent, "plugin_name"_a, "key"_a, + "value"_a, "sync"_a = true) + .def("pluginDataPath", &IOrganizer::pluginDataPath) + .def("installMod", wrap_for_filepath<1>(&IOrganizer::installMod), + py::return_value_policy::reference, "filename"_a, + "name_suggestion"_a = "") + .def("resolvePath", wrap_for_filepath(&IOrganizer::resolvePath), + "filename"_a) + .def("listDirectories", &IOrganizer::listDirectories, "directory"_a) + + // "provide multiple overloads of findFiles + .def( + "findFiles", + [](const IOrganizer* o, DirectoryWrapper const& p, + std::function const& f) { + return o->findFiles(p, f); + }, + "path"_a, "filter"_a) + + // in C++, it is possible to create a QStringList implicitly from + // a single QString, in Python is not possible with the current + // converters in python (and I do not think it is a good idea to + // have it everywhere), but here it is nice to be able to + // pass a single string, so we add an extra overload + // + // important: the order matters, because a Python string can be + // converted to a QStringList since it is a sequence of + // single-character strings: + .def( + "findFiles", + [](const IOrganizer* o, DirectoryWrapper const& p, + const QStringList& gf) { + return o->findFiles(p, gf); + }, + "path"_a, "patterns"_a) + .def( + "findFiles", + [](const IOrganizer* o, DirectoryWrapper const& p, const QString& f) { + return o->findFiles(p, QStringList{f}); + }, + "path"_a, "pattern"_a) + + .def("getFileOrigins", &IOrganizer::getFileOrigins, "filename"_a) + .def("findFileInfos", wrap_for_directory(&IOrganizer::findFileInfos), + "path"_a, "filter"_a) + + .def("virtualFileTree", &IOrganizer::virtualFileTree) + + .def("instanceManager", &IOrganizer::instanceManager, + py::return_value_policy::reference) + .def("downloadManager", &IOrganizer::downloadManager, + py::return_value_policy::reference) + .def("pluginList", &IOrganizer::pluginList, + py::return_value_policy::reference) + .def("modList", &IOrganizer::modList, py::return_value_policy::reference) + .def("executablesList", &IOrganizer::executablesList, + py::return_value_policy::reference) + .def("gameFeatures", &IOrganizer::gameFeatures, + py::return_value_policy::reference) + .def("profile", &IOrganizer::profile) + .def("profileNames", &IOrganizer::profileNames) + .def("getProfile", &IOrganizer::getProfile, "name"_a) + + // custom implementation for startApplication and + // waitForApplication because 1) HANDLE (= void*) is not properly + // converted from/to python, and 2) we need to convert the by-ptr + // argument to a return-tuple for waitForApplication + .def( + "startApplication", + [](IOrganizer* o, const FileWrapper& executable, + const QStringList& args, const DirectoryWrapper& cwd, + const QString& profile, const QString& forcedCustomOverwrite, + bool ignoreCustomOverwrite) -> std::uintptr_t { + return (std::uintptr_t)o->startApplication( + executable, args, cwd, profile, forcedCustomOverwrite, + ignoreCustomOverwrite); + }, + "executable"_a, "args"_a = QStringList(), "cwd"_a = "", + "profile"_a = "", "forcedCustomOverwrite"_a = "", + "ignoreCustomOverwrite"_a = false) + .def( + "waitForApplication", + [](IOrganizer* o, std::uintptr_t handle, bool refresh) { + DWORD returnCode; + bool result = + o->waitForApplication((HANDLE)handle, refresh, &returnCode); + + // we force signed return code because it's probably what's expected + // in Python + return std::make_tuple( + result, static_cast>(returnCode)); + }, + "handle"_a, "refresh"_a = true) + + .def("refresh", &IOrganizer::refresh, "save_changes"_a = true) + .def("managedGame", &IOrganizer::managedGame, + py::return_value_policy::reference) + + .def("onAboutToRun", + py::overload_cast const&>( + &IOrganizer::onAboutToRun), + "callback"_a) + .def("onAboutToRun", + py::overload_cast const&>( + &IOrganizer::onAboutToRun), + "callback"_a) + .def("onFinishedRun", &IOrganizer::onFinishedRun, "callback"_a) + .def("onUserInterfaceInitialized", &IOrganizer::onUserInterfaceInitialized, + "callback"_a) + .def("onNextRefresh", &IOrganizer::onNextRefresh, "callback"_a, + "immediate_if_possible"_a = true) + .def("onProfileCreated", &IOrganizer::onProfileCreated, "callback"_a) + .def("onProfileRenamed", &IOrganizer::onProfileRenamed, "callback"_a) + .def("onProfileRemoved", &IOrganizer::onProfileRemoved, "callback"_a) + .def("onProfileChanged", &IOrganizer::onProfileChanged, "callback"_a) + + .def("onPluginSettingChanged", &IOrganizer::onPluginSettingChanged, + "callback"_a) + .def( + "onPluginEnabled", + [](IOrganizer* o, std::function const& func) { + o->onPluginEnabled(func); + }, + "callback"_a) + .def( + "onPluginEnabled", + [](IOrganizer* o, QString const& name, + std::function const& func) { + o->onPluginEnabled(name, func); + }, + "name"_a, "callback"_a) + .def( + "onPluginDisabled", + [](IOrganizer* o, std::function const& func) { + o->onPluginDisabled(func); + }, + "callback"_a) + .def( + "onPluginDisabled", + [](IOrganizer* o, QString const& name, + std::function const& func) { + o->onPluginDisabled(name, func); + }, + "name"_a, "callback"_a) + + // DEPRECATED: + .def( + "getMod", + [](IOrganizer* o, QString const& name) { + mo2::python::show_deprecation_warning( + "getMod", "IOrganizer::getMod(str) is deprecated, use " + "IModList::getMod(str) instead."); + return o->modList()->getMod(name); + }, + py::return_value_policy::reference, "name"_a) + .def( + "removeMod", + [](IOrganizer* o, IModInterface* mod) { + mo2::python::show_deprecation_warning( + "removeMod", + "IOrganizer::removeMod(IModInterface) is deprecated, use " + "IModList::removeMod(IModInterface) instead."); + return o->modList()->removeMod(mod); + }, + "mod"_a) + .def("modsSortedByProfilePriority", + [](IOrganizer* o) { + mo2::python::show_deprecation_warning( + "modsSortedByProfilePriority", + "IOrganizer::modsSortedByProfilePriority() is deprecated, use " + "IModList::allModsByProfilePriority() instead."); + return o->modList()->allModsByProfilePriority(); + }) + .def( + "refreshModList", + [](IOrganizer* o, bool s) { + mo2::python::show_deprecation_warning( + "refreshModList", + "IOrganizer::refreshModList(bool) is deprecated, use " + "IOrganizer::refresh(bool) instead."); + o->refresh(s); + }, + "save_changes"_a = true) + .def( + "onModInstalled", + [](IOrganizer* organizer, + const std::function& func) { + mo2::python::show_deprecation_warning( + "onModInstalled", + "IOrganizer::onModInstalled(Callable[[str], None]) is " + "deprecated, " + "use IModList::onModInstalled(Callable[[IModInterface], None]) " + "instead."); + return organizer->modList()->onModInstalled( + [func](MOBase::IModInterface* m) { + func(m->name()); + }); + ; + }, + "callback"_a) + + .def_static("getPluginDataPath", &IOrganizer::getPluginDataPath); + } + + void add_iinstance_manager_classes(py::module_ m) + { + py::class_>(m, "IInstance") + .def("displayName", &IInstance::displayName) + .def("gameName", &IInstance::gameName) + .def("gameDirectory", &IInstance::gameDirectory) + .def("isPortable", &IInstance::isPortable); + + py::class_(m, "IInstanceManager") + .def("currentInstance", &IInstanceManager::currentInstance) + .def("globalInstancePaths", &IInstanceManager::globalInstancePaths) + .def("getGlobalInstance", &IInstanceManager::getGlobalInstance); + } + + void add_idownload_manager_classes(py::module_ m) + { + py::class_(m, "IDownloadManager") + .def("startDownloadURLs", &IDownloadManager::startDownloadURLs, "urls"_a) + .def("startDownloadNexusFile", &IDownloadManager::startDownloadNexusFile, + "mod_id"_a, "file_id"_a) + .def("startDownloadNexusFileForGame", + &IDownloadManager::startDownloadNexusFileForGame, "game_name"_a, + "mod_id"_a, "file_id"_a) + .def("downloadPath", &IDownloadManager::downloadPath, "id"_a) + .def("onDownloadComplete", &IDownloadManager::onDownloadComplete, + "callback"_a) + .def("onDownloadPaused", &IDownloadManager::onDownloadPaused, "callback"_a) + .def("onDownloadFailed", &IDownloadManager::onDownloadFailed, "callback"_a) + .def("onDownloadRemoved", &IDownloadManager::onDownloadRemoved, + "callback"_a); + } + + void add_iinstallation_manager_classes(py::module_ m) + { + // add this here to get proper typing + py::enum_(m, "InstallResult") + .value("SUCCESS", IPluginInstaller::RESULT_SUCCESS) + .value("FAILED", IPluginInstaller::RESULT_FAILED) + .value("CANCELED", IPluginInstaller::RESULT_CANCELED) + .value("MANUAL_REQUESTED", IPluginInstaller::RESULT_MANUALREQUESTED) + .value("NOT_ATTEMPTED", IPluginInstaller::RESULT_NOTATTEMPTED); + + py::class_(m, "IInstallationManager") + .def("getSupportedExtensions", + &IInstallationManager::getSupportedExtensions) + .def("extractFile", &IInstallationManager::extractFile, "entry"_a, + "silent"_a = false) + .def("extractFiles", &IInstallationManager::extractFiles, "entries"_a, + "silent"_a = false) + .def("createFile", &IInstallationManager::createFile, "entry"_a) + + // return a tuple to get back the mod name and the mod ID + .def( + "installArchive", + [](IInstallationManager* m, GuessedValue modName, + FileWrapper archive, int modId) { + auto result = m->installArchive(modName, archive, modId); + return std::make_tuple(result, static_cast(modName), + modId); + }, + "mod_name"_a, "archive"_a, "mod_id"_a = 0); + } + + void add_basic_bindings(py::module_ m) + { + add_versioninfo_classes(m); + add_executable_classes(m); + add_guessedstring_classes(m); + + add_ifiletree_bindings(m); + + add_modinterface_classes(m); + add_modrepository_classes(m); + + py::class_(m, "PluginSetting") + .def(py::init(), "key"_a, + "description"_a, "default_value"_a) + .def_readwrite("key", &PluginSetting::key) + .def_readwrite("description", &PluginSetting::description) + .def_readwrite("default_value", &PluginSetting::defaultValue); + + py::class_(m, "PluginRequirementFactory") + // pluginDependency + .def_static("pluginDependency", + py::overload_cast( + &PluginRequirementFactory::pluginDependency), + "plugins"_a) + .def_static("pluginDependency", + py::overload_cast( + &PluginRequirementFactory::pluginDependency), + "plugin"_a) + // gameDependency + .def_static("gameDependency", + py::overload_cast( + &PluginRequirementFactory::gameDependency), + "games"_a) + .def_static("gameDependency", + py::overload_cast( + &PluginRequirementFactory::gameDependency), + "game"_a) + // diagnose + .def_static("diagnose", &PluginRequirementFactory::diagnose, "diagnose"_a) + // basic + .def_static("basic", &PluginRequirementFactory::basic, "checker"_a, + "description"_a); + + py::class_(m, "Mapping") + .def(py::init<>()) + .def(py::init([](QString src, QString dst, bool dir, bool crt) -> Mapping { + return {src, dst, dir, crt}; + }), + "source"_a, "destination"_a, "is_directory"_a, + "create_target"_a = false) + .def_readwrite("source", &Mapping::source) + .def_readwrite("destination", &Mapping::destination) + .def_readwrite("isDirectory", &Mapping::isDirectory) + .def_readwrite("createTarget", &Mapping::createTarget) + .def("__str__", [](Mapping const& m) { + return std::format(L"Mapping({}, {}, {}, {})", m.source.toStdWString(), + m.destination.toStdWString(), m.isDirectory, + m.createTarget); + }); + + // must be done BEFORE imodlist because there is a default argument to a + // IProfile* in the modlist class + py::class_>(m, "IProfile") + .def("name", &IProfile::name) + .def("absolutePath", &IProfile::absolutePath) + .def("localSavesEnabled", &IProfile::localSavesEnabled) + .def("localSettingsEnabled", &IProfile::localSettingsEnabled) + .def("invalidationActive", + [](const IProfile* p) { + bool supported; + bool active = p->invalidationActive(&supported); + return std::make_tuple(active, supported); + }) + .def("absoluteIniFilePath", &IProfile::absoluteIniFilePath, "inifile"_a); + + add_ipluginlist_classes(m); + add_imodlist_classes(m); + add_iinstance_manager_classes(m); + add_idownload_manager_classes(m); + add_iinstallation_manager_classes(m); + add_iorganizer_classes(m); + } + +} // namespace mo2::python diff --git a/libs/plugin_python/src/mobase/wrappers/game_features.cpp b/libs/plugin_python/src/mobase/wrappers/game_features.cpp new file mode 100644 index 0000000..655334b --- /dev/null +++ b/libs/plugin_python/src/mobase/wrappers/game_features.cpp @@ -0,0 +1,422 @@ +#include "wrappers.h" + +#include + +#include "../pybind11_all.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pyfiletree.h" + +namespace py = pybind11; + +using namespace MOBase; +using namespace pybind11::literals; + +namespace mo2::python { + + class PyBSAInvalidation : public BSAInvalidation { + public: + bool isInvalidationBSA(const QString& bsaName) override + { + PYBIND11_OVERRIDE_PURE(bool, BSAInvalidation, isInvalidationBSA, bsaName); + } + void deactivate(MOBase::IProfile* profile) override + { + PYBIND11_OVERRIDE_PURE(void, BSAInvalidation, deactivate, profile); + } + void activate(MOBase::IProfile* profile) override + { + PYBIND11_OVERRIDE_PURE(void, BSAInvalidation, activate, profile); + } + bool prepareProfile(MOBase::IProfile* profile) override + { + PYBIND11_OVERRIDE_PURE(bool, BSAInvalidation, prepareProfile, profile); + } + }; + + class PyDataArchives : public DataArchives { + public: + QStringList vanillaArchives() const override + { + PYBIND11_OVERRIDE_PURE(QStringList, DataArchives, vanillaArchives, ); + } + QStringList archives(const MOBase::IProfile* profile) const override + { + PYBIND11_OVERRIDE_PURE(QStringList, DataArchives, archives, profile); + } + void addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) override + { + PYBIND11_OVERRIDE_PURE(void, DataArchives, addArchive, profile, index, + archiveName); + } + void removeArchive(MOBase::IProfile* profile, + const QString& archiveName) override + { + PYBIND11_OVERRIDE_PURE(void, DataArchives, removeArchive, profile, + archiveName); + } + }; + + class PyGamePlugins : public GamePlugins { + public: + void writePluginLists(const MOBase::IPluginList* pluginList) override + { + PYBIND11_OVERRIDE_PURE(void, GamePlugins, writePluginLists, pluginList); + } + void readPluginLists(MOBase::IPluginList* pluginList) override + { + // TODO: cannot update plugin list or create one from Python so this is + // useless + PYBIND11_OVERRIDE_PURE(void, GamePlugins, readPluginLists, pluginList); + } + QStringList getLoadOrder() override + { + PYBIND11_OVERRIDE_PURE(QStringList, GamePlugins, getLoadOrder, ); + } + bool lightPluginsAreSupported() override + { + PYBIND11_OVERRIDE(bool, GamePlugins, lightPluginsAreSupported, ); + } + bool mediumPluginsAreSupported() override + { + PYBIND11_OVERRIDE(bool, GamePlugins, mediumPluginsAreSupported, ); + } + bool blueprintPluginsAreSupported() override + { + PYBIND11_OVERRIDE(bool, GamePlugins, blueprintPluginsAreSupported, ); + } + }; + + class PyLocalSavegames : public LocalSavegames { + public: + MappingType mappings(const QDir& profileSaveDir) const override + { + PYBIND11_OVERRIDE_PURE(MappingType, LocalSavegames, mappings, + profileSaveDir); + } + bool prepareProfile(MOBase::IProfile* profile) override + { + PYBIND11_OVERRIDE_PURE(bool, LocalSavegames, prepareProfile, profile); + } + }; + + class PyModDataChecker : public ModDataChecker { + public: + CheckReturn + dataLooksValid(std::shared_ptr fileTree) const override + { + PYBIND11_OVERRIDE_PURE(CheckReturn, ModDataChecker, dataLooksValid, + fileTree); + } + + std::shared_ptr + fix(std::shared_ptr fileTree) const override + { + PYBIND11_OVERRIDE(std::shared_ptr, ModDataChecker, fix, + fileTree); + } + }; + + class PyModDataContent : public ModDataContent { + public: + std::vector getAllContents() const override + { + PYBIND11_OVERRIDE_PURE(std::vector, ModDataContent, + getAllContents, ); + ; + } + std::vector + getContentsFor(std::shared_ptr fileTree) const override + { + PYBIND11_OVERRIDE_PURE(std::vector, ModDataContent, getContentsFor, + fileTree); + } + }; + + class PySaveGameInfo : public SaveGameInfo { + public: + MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const override + { + PYBIND11_OVERRIDE_PURE(MissingAssets, SaveGameInfo, getMissingAssets, + &save); + } + ISaveGameInfoWidget* getSaveGameWidget(QWidget* parent = 0) const override + { + PYBIND11_OVERRIDE_PURE(ISaveGameInfoWidget*, SaveGameInfo, + getSaveGameWidget, parent); + } + }; + + class PyScriptExtender : public ScriptExtender { + public: + QString BinaryName() const override + { + PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, binaryName, ); + } + + QString PluginPath() const override + { + PYBIND11_OVERRIDE_PURE(DirectoryWrapper, ScriptExtender, pluginPath, ); + } + + QString loaderName() const override + { + PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, loaderName, ); + } + + QString loaderPath() const override + { + PYBIND11_OVERRIDE_PURE(FileWrapper, ScriptExtender, loaderPath, ); + } + + QString savegameExtension() const override + { + PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, savegameExtension, ); + } + + bool isInstalled() const override + { + PYBIND11_OVERRIDE_PURE(bool, ScriptExtender, isInstalled, ); + } + + QString getExtenderVersion() const override + { + PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, getExtenderVersion, ); + } + + WORD getArch() const override + { + PYBIND11_OVERRIDE_PURE(WORD, ScriptExtender, getArch, ); + } + }; + + class PyUnmanagedMods : public UnmanagedMods { + public: + QStringList mods(bool onlyOfficial) const override + { + PYBIND11_OVERRIDE_PURE(QStringList, UnmanagedMods, mods, onlyOfficial); + } + QString displayName(const QString& modName) const override + { + PYBIND11_OVERRIDE_PURE(QString, UnmanagedMods, displayName, modName); + } + QFileInfo referenceFile(const QString& modName) const override + { + PYBIND11_OVERRIDE_PURE(FileWrapper, UnmanagedMods, referenceFile, modName); + } + QStringList secondaryFiles(const QString& modName) const override + { + return toQStringList([&] { + PYBIND11_OVERRIDE_PURE(QList, UnmanagedMods, + secondaryFiles, modName); + }()); + } + }; + + void add_game_feature_bindings(pybind11::module_ m) + { + // this is just to allow accepting GameFeature in function, we do not expose + // anything from game feature to Python since typeInfo() is useless in Python + // + py::class_>(m, "GameFeature"); + + // BSAInvalidation + + py::class_>(m, "BSAInvalidation") + .def(py::init<>()) + .def("isInvalidationBSA", &BSAInvalidation::isInvalidationBSA, "name"_a) + .def("deactivate", &BSAInvalidation::deactivate, "profile"_a) + .def("activate", &BSAInvalidation::activate, "profile"_a); + + // DataArchives + + py::class_>(m, "DataArchives") + .def(py::init<>()) + .def("vanillaArchives", &DataArchives::vanillaArchives) + .def("archives", &DataArchives::archives, "profile"_a) + .def("addArchive", &DataArchives::addArchive, "profile"_a, "index"_a, + "name"_a) + .def("removeArchive", &DataArchives::removeArchive, "profile"_a, "name"_a); + + // GamePlugins + + py::class_>(m, "GamePlugins") + .def(py::init<>()) + .def("writePluginLists", &GamePlugins::writePluginLists, "plugin_list"_a) + .def("readPluginLists", &GamePlugins::readPluginLists, "plugin_list"_a) + .def("getLoadOrder", &GamePlugins::getLoadOrder) + .def("lightPluginsAreSupported", &GamePlugins::lightPluginsAreSupported) + .def("mediumPluginsAreSupported", &GamePlugins::mediumPluginsAreSupported) + .def("blueprintPluginsAreSupported", + &GamePlugins::blueprintPluginsAreSupported); + + // LocalSavegames + + py::class_>(m, "LocalSavegames") + .def(py::init<>()) + .def("mappings", &LocalSavegames::mappings, "profile_save_dir"_a) + .def("prepareProfile", &LocalSavegames::prepareProfile, "profile"_a); + + // ModDataChecker + + py::class_> + pyModDataChecker(m, "ModDataChecker"); + + py::enum_(pyModDataChecker, "CheckReturn") + .value("INVALID", ModDataChecker::CheckReturn::INVALID) + .value("FIXABLE", ModDataChecker::CheckReturn::FIXABLE) + .value("VALID", ModDataChecker::CheckReturn::VALID) + .export_values(); + + pyModDataChecker.def(py::init<>()) + .def("dataLooksValid", &ModDataChecker::dataLooksValid, "filetree"_a) + .def("fix", &ModDataChecker::fix, "filetree"_a); + + // ModDataContent + py::class_> + pyModDataContent(m, "ModDataContent"); + + py::class_(pyModDataContent, "Content") + .def(py::init(), "id"_a, "name"_a, "icon"_a, + "filter_only"_a = false) + .def_property_readonly("id", &ModDataContent::Content::id) + .def_property_readonly("name", &ModDataContent::Content::name) + .def_property_readonly("icon", &ModDataContent::Content::icon) + .def("isOnlyForFilter", &ModDataContent::Content::isOnlyForFilter); + + pyModDataContent.def(py::init<>()) + .def("getAllContents", &ModDataContent::getAllContents) + .def("getContentsFor", &ModDataContent::getContentsFor, "filetree"_a); + + // SaveGameInfo + + py::class_>(m, "SaveGameInfo") + .def(py::init<>()) + .def("getMissingAssets", &SaveGameInfo::getMissingAssets, "save"_a) + .def("getSaveGameWidget", &SaveGameInfo::getSaveGameWidget, + py::return_value_policy::reference, "parent"_a); + + // ScriptExtender + + py::class_>(m, "ScriptExtender") + .def(py::init<>()) + .def("binaryName", &ScriptExtender::BinaryName) + .def("pluginPath", wrap_return_for_directory(&ScriptExtender::PluginPath)) + .def("loaderName", &ScriptExtender::loaderName) + .def("loaderPath", wrap_return_for_filepath(&ScriptExtender::loaderPath)) + .def("savegameExtension", &ScriptExtender::savegameExtension) + .def("isInstalled", &ScriptExtender::isInstalled) + .def("getExtenderVersion", &ScriptExtender::getExtenderVersion) + .def("getArch", &ScriptExtender::getArch); + + // UnmanagedMods + + py::class_>(m, "UnmanagedMods") + .def(py::init<>()) + .def("mods", &UnmanagedMods::mods, "official_only"_a) + .def("displayName", &UnmanagedMods::displayName, "mod_name"_a) + .def("referenceFile", + wrap_return_for_filepath(&UnmanagedMods::referenceFile), "mod_name"_a) + .def( + "secondaryFiles", + [](UnmanagedMods* m, const QString& modName) -> QList { + auto result = m->secondaryFiles(modName); + return {result.begin(), result.end()}; + }, + "mod_name"_a); + } + + void add_igamefeatures_classes(py::module_ m) + { + py::class_(m, "IGameFeatures") + .def("registerFeature", + py::overload_cast, + int, bool>(&IGameFeatures::registerFeature), + "games"_a, "feature"_a, "priority"_a, "replace"_a = false) + .def("registerFeature", + py::overload_cast, + int, bool>(&IGameFeatures::registerFeature), + "game"_a, "feature"_a, "priority"_a, "replace"_a = false) + .def("registerFeature", + py::overload_cast, int, bool>( + &IGameFeatures::registerFeature), + "feature"_a, "priority"_a, "replace"_a = false) + .def("unregisterFeature", &IGameFeatures::unregisterFeature, "feature"_a) + .def("unregisterFeatures", &unregister_feature, "feature_type"_a) + .def("gameFeature", &extract_feature, "feature_type"_a, + py ::return_value_policy::reference); + } + +} // namespace mo2::python + +namespace mo2::python { + + class GameFeaturesHelper { + template + static void helper(F&& f, std::index_sequence) + { + (f(static_cast< + std::tuple_element_t>( + nullptr)), + ...); + } + + public: + // apply the function f on a null-pointer of type Feature* for each game + // feature + template + static void apply(F&& f) + { + helper(f, std::make_index_sequence< + std::tuple_size_v>{}); + } + }; + + pybind11::object extract_feature(IGameFeatures const& gameFeatures, + pybind11::object type) + { + py::object py_feature = py::none(); + GameFeaturesHelper::apply([&](Feature*) { + if (py::type::of().is(type)) { + py_feature = py::cast(gameFeatures.gameFeature(), + py::return_value_policy::reference); + } + }); + return py_feature; + } + + int unregister_feature(MOBase::IGameFeatures& gameFeatures, pybind11::object type) + { + int count = 0; + GameFeaturesHelper::apply([&](Feature*) { + if (py::type::of().is(type)) { + count = gameFeatures.unregisterFeatures(); + } + }); + return count; + } + +} // namespace mo2::python diff --git a/libs/plugin_python/src/mobase/wrappers/known_folders.h b/libs/plugin_python/src/mobase/wrappers/known_folders.h new file mode 100644 index 0000000..f74238b --- /dev/null +++ b/libs/plugin_python/src/mobase/wrappers/known_folders.h @@ -0,0 +1,155 @@ +#include + +namespace mo2::python { + + struct KnownFolder { + const char* name; + KNOWNFOLDERID guid; + }; + + const std::array KNOWN_FOLDERS{{ + {"AccountPictures", FOLDERID_AccountPictures}, + {"AddNewPrograms", FOLDERID_AddNewPrograms}, + {"AdminTools", FOLDERID_AdminTools}, + {"AllAppMods", FOLDERID_AllAppMods}, + {"AppCaptures", FOLDERID_AppCaptures}, + {"AppDataDesktop", FOLDERID_AppDataDesktop}, + {"AppDataDocuments", FOLDERID_AppDataDocuments}, + {"AppDataFavorites", FOLDERID_AppDataFavorites}, + {"AppDataProgramData", FOLDERID_AppDataProgramData}, + {"ApplicationShortcuts", FOLDERID_ApplicationShortcuts}, + {"AppsFolder", FOLDERID_AppsFolder}, + {"AppUpdates", FOLDERID_AppUpdates}, + {"CameraRoll", FOLDERID_CameraRoll}, + {"CameraRollLibrary", FOLDERID_CameraRollLibrary}, + {"CDBurning", FOLDERID_CDBurning}, + {"ChangeRemovePrograms", FOLDERID_ChangeRemovePrograms}, + {"CommonAdminTools", FOLDERID_CommonAdminTools}, + {"CommonOEMLinks", FOLDERID_CommonOEMLinks}, + {"CommonPrograms", FOLDERID_CommonPrograms}, + {"CommonStartMenu", FOLDERID_CommonStartMenu}, + {"CommonStartMenuPlaces", FOLDERID_CommonStartMenuPlaces}, + {"CommonStartup", FOLDERID_CommonStartup}, + {"CommonTemplates", FOLDERID_CommonTemplates}, + {"ComputerFolder", FOLDERID_ComputerFolder}, + {"ConflictFolder", FOLDERID_ConflictFolder}, + {"ConnectionsFolder", FOLDERID_ConnectionsFolder}, + {"Contacts", FOLDERID_Contacts}, + {"ControlPanelFolder", FOLDERID_ControlPanelFolder}, + {"Cookies", FOLDERID_Cookies}, + {"CurrentAppMods", FOLDERID_CurrentAppMods}, + {"Desktop", FOLDERID_Desktop}, + {"DevelopmentFiles", FOLDERID_DevelopmentFiles}, + {"Device", FOLDERID_Device}, + {"DeviceMetadataStore", FOLDERID_DeviceMetadataStore}, + {"Documents", FOLDERID_Documents}, + {"DocumentsLibrary", FOLDERID_DocumentsLibrary}, + {"Downloads", FOLDERID_Downloads}, + {"Favorites", FOLDERID_Favorites}, + {"Fonts", FOLDERID_Fonts}, + {"Games", FOLDERID_Games}, + {"GameTasks", FOLDERID_GameTasks}, + {"History", FOLDERID_History}, + {"HomeGroup", FOLDERID_HomeGroup}, + {"HomeGroupCurrentUser", FOLDERID_HomeGroupCurrentUser}, + {"ImplicitAppShortcuts", FOLDERID_ImplicitAppShortcuts}, + {"InternetCache", FOLDERID_InternetCache}, + {"InternetFolder", FOLDERID_InternetFolder}, + {"Libraries", FOLDERID_Libraries}, + {"Links", FOLDERID_Links}, + {"LocalAppData", FOLDERID_LocalAppData}, + {"LocalAppDataLow", FOLDERID_LocalAppDataLow}, + {"LocalDocuments", FOLDERID_LocalDocuments}, + {"LocalDownloads", FOLDERID_LocalDownloads}, + {"LocalizedResourcesDir", FOLDERID_LocalizedResourcesDir}, + {"LocalMusic", FOLDERID_LocalMusic}, + {"LocalPictures", FOLDERID_LocalPictures}, + {"LocalStorage", FOLDERID_LocalStorage}, + {"LocalVideos", FOLDERID_LocalVideos}, + {"Music", FOLDERID_Music}, + {"MusicLibrary", FOLDERID_MusicLibrary}, + {"NetHood", FOLDERID_NetHood}, + {"NetworkFolder", FOLDERID_NetworkFolder}, + {"Objects3D", FOLDERID_Objects3D}, + {"OneDrive", FOLDERID_OneDrive}, + {"OriginalImages", FOLDERID_OriginalImages}, + {"PhotoAlbums", FOLDERID_PhotoAlbums}, + {"Pictures", FOLDERID_Pictures}, + {"PicturesLibrary", FOLDERID_PicturesLibrary}, + {"Playlists", FOLDERID_Playlists}, + {"PrintersFolder", FOLDERID_PrintersFolder}, + {"PrintHood", FOLDERID_PrintHood}, + {"Profile", FOLDERID_Profile}, + {"ProgramData", FOLDERID_ProgramData}, + {"ProgramFiles", FOLDERID_ProgramFiles}, + {"ProgramFilesCommon", FOLDERID_ProgramFilesCommon}, + {"ProgramFilesCommonX64", FOLDERID_ProgramFilesCommonX64}, + {"ProgramFilesCommonX86", FOLDERID_ProgramFilesCommonX86}, + {"ProgramFilesX64", FOLDERID_ProgramFilesX64}, + {"ProgramFilesX86", FOLDERID_ProgramFilesX86}, + {"Programs", FOLDERID_Programs}, + {"Public", FOLDERID_Public}, + {"PublicDesktop", FOLDERID_PublicDesktop}, + {"PublicDocuments", FOLDERID_PublicDocuments}, + {"PublicDownloads", FOLDERID_PublicDownloads}, + {"PublicGameTasks", FOLDERID_PublicGameTasks}, + {"PublicLibraries", FOLDERID_PublicLibraries}, + {"PublicMusic", FOLDERID_PublicMusic}, + {"PublicPictures", FOLDERID_PublicPictures}, + {"PublicRingtones", FOLDERID_PublicRingtones}, + {"PublicUserTiles", FOLDERID_PublicUserTiles}, + {"PublicVideos", FOLDERID_PublicVideos}, + {"QuickLaunch", FOLDERID_QuickLaunch}, + {"Recent", FOLDERID_Recent}, + {"RecordedCalls", FOLDERID_RecordedCalls}, + {"RecordedTVLibrary", FOLDERID_RecordedTVLibrary}, + {"RecycleBinFolder", FOLDERID_RecycleBinFolder}, + {"ResourceDir", FOLDERID_ResourceDir}, + {"RetailDemo", FOLDERID_RetailDemo}, + {"Ringtones", FOLDERID_Ringtones}, + {"RoamedTileImages", FOLDERID_RoamedTileImages}, + {"RoamingAppData", FOLDERID_RoamingAppData}, + {"RoamingTiles", FOLDERID_RoamingTiles}, + {"SampleMusic", FOLDERID_SampleMusic}, + {"SamplePictures", FOLDERID_SamplePictures}, + {"SamplePlaylists", FOLDERID_SamplePlaylists}, + {"SampleVideos", FOLDERID_SampleVideos}, + {"SavedGames", FOLDERID_SavedGames}, + {"SavedPictures", FOLDERID_SavedPictures}, + {"SavedPicturesLibrary", FOLDERID_SavedPicturesLibrary}, + {"SavedSearches", FOLDERID_SavedSearches}, + {"Screenshots", FOLDERID_Screenshots}, + {"SEARCH_CSC", FOLDERID_SEARCH_CSC}, + {"SEARCH_MAPI", FOLDERID_SEARCH_MAPI}, + {"SearchHistory", FOLDERID_SearchHistory}, + {"SearchHome", FOLDERID_SearchHome}, + {"SearchTemplates", FOLDERID_SearchTemplates}, + {"SendTo", FOLDERID_SendTo}, + {"SidebarDefaultParts", FOLDERID_SidebarDefaultParts}, + {"SidebarParts", FOLDERID_SidebarParts}, + {"SkyDrive", FOLDERID_SkyDrive}, + {"SkyDriveCameraRoll", FOLDERID_SkyDriveCameraRoll}, + {"SkyDriveDocuments", FOLDERID_SkyDriveDocuments}, + {"SkyDriveMusic", FOLDERID_SkyDriveMusic}, + {"SkyDrivePictures", FOLDERID_SkyDrivePictures}, + {"StartMenu", FOLDERID_StartMenu}, + {"StartMenuAllPrograms", FOLDERID_StartMenuAllPrograms}, + {"Startup", FOLDERID_Startup}, + {"SyncManagerFolder", FOLDERID_SyncManagerFolder}, + {"SyncResultsFolder", FOLDERID_SyncResultsFolder}, + {"SyncSetupFolder", FOLDERID_SyncSetupFolder}, + {"System", FOLDERID_System}, + {"SystemX86", FOLDERID_SystemX86}, + {"Templates", FOLDERID_Templates}, + {"UserPinned", FOLDERID_UserPinned}, + {"UserProfiles", FOLDERID_UserProfiles}, + {"UserProgramFiles", FOLDERID_UserProgramFiles}, + {"UserProgramFilesCommon", FOLDERID_UserProgramFilesCommon}, + {"UsersFiles", FOLDERID_UsersFiles}, + {"UsersLibraries", FOLDERID_UsersLibraries}, + {"Videos", FOLDERID_Videos}, + {"VideosLibrary", FOLDERID_VideosLibrary}, + {"Windows", FOLDERID_Windows}, + }}; + +} // namespace mo2::python diff --git a/libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp b/libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp new file mode 100644 index 0000000..6dacf26 --- /dev/null +++ b/libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp @@ -0,0 +1,329 @@ +#include "pyfiletree.h" + +#include +#include + +#include "../pybind11_all.h" + +#include +#include + +namespace py = pybind11; +using namespace MOBase; + +namespace mo2::detail { + + // filetree implementation for testing purpose + // + class PyFileTree : public IFileTree { + public: + using callback_t = std::function; + + PyFileTree(std::shared_ptr parent, QString name, + callback_t callback) + : FileTreeEntry(parent, name), IFileTree(), m_Callback(callback) + { + } + + std::shared_ptr addFile(QString name, bool) override + { + if (m_Callback && !m_Callback(name, false)) { + throw UnsupportedOperationException("File rejected by callback."); + } + return IFileTree::addFile(name); + } + + std::shared_ptr addDirectory(QString name) override + { + if (m_Callback && !m_Callback(name, true)) { + throw UnsupportedOperationException("Directory rejected by callback."); + } + return IFileTree::addDirectory(name); + } + + protected: + std::shared_ptr + makeDirectory(std::shared_ptr parent, + QString name) const override + { + return std::make_shared(parent, name, m_Callback); + } + + bool doPopulate([[maybe_unused]] std::shared_ptr parent, + std::vector>&) const override + { + return true; + } + std::shared_ptr doClone() const override + { + return std::make_shared(nullptr, name(), m_Callback); + } + + private: + callback_t m_Callback; + }; + +} // namespace mo2::detail + +#pragma optimize("", off) + +namespace pybind11 { + const void* polymorphic_type_hook::get(const FileTreeEntry* src, + const std::type_info*& type) + { + if (auto p = dynamic_cast(src)) { + type = &typeid(IFileTree); + return p; + } + return src; + } +} // namespace pybind11 + +namespace mo2::python { + + void add_ifiletree_bindings(pybind11::module_& m) + { + // FileTreeEntry class: + auto fileTreeEntryClass = + py::class_>(m, + "FileTreeEntry"); + + // IFileTree class: + auto iFileTreeClass = + py::class_>( + m, "IFileTree", py::multiple_inheritance()); + + // this is FILE_OR_DIRECTORY but as a FileType since we kind of cheat for the + // exposure in Python and this help pybind11 creates proper typing + + const auto FILE_OR_DIRECTORY = static_cast( + FileTreeEntry::FILE_OR_DIRECTORY.toInt()); + + // we do not use the enum directly, we will mostly bind the FileTypes + // (with an S) + py::enum_(fileTreeEntryClass, "FileTypes", + py::arithmetic{}) + .value("FILE", FileTreeEntry::FileType::FILE) + .value("DIRECTORY", FileTreeEntry::FileType::DIRECTORY) + .value("FILE_OR_DIRECTORY", FILE_OR_DIRECTORY) + .export_values(); + + fileTreeEntryClass + + .def("isFile", &FileTreeEntry::isFile) + .def("isDir", &FileTreeEntry::isDir) + .def("fileType", &FileTreeEntry::fileType) + .def("name", &FileTreeEntry::name) + .def("suffix", &FileTreeEntry::suffix) + .def( + "hasSuffix", + [](FileTreeEntry* entry, QStringList suffixes) { + return entry->hasSuffix(suffixes); + }, + py::arg("suffixes")) + .def( + "hasSuffix", + [](FileTreeEntry* entry, QString suffix) { + return entry->hasSuffix(suffix); + }, + py::arg("suffix")) + .def("parent", py::overload_cast<>(&FileTreeEntry::parent)) + .def("path", &FileTreeEntry::path, py::arg("sep") = "\\") + .def("pathFrom", &FileTreeEntry::pathFrom, py::arg("tree"), + py::arg("sep") = "\\") + + // Mutable operation: + .def("detach", &FileTreeEntry::detach) + .def("moveTo", &FileTreeEntry::moveTo, py::arg("tree")) + + // Special methods: + .def("__eq__", + [](const FileTreeEntry* entry, QString other) { + return entry->compare(other) == 0; + }) + .def("__eq__", + [](const FileTreeEntry* entry, std::shared_ptr other) { + return entry == other.get(); + }) + + // Special methods for debug: + .def("__repr__", [](const FileTreeEntry* entry) { + return "FileTreeEntry(\"" + entry->name() + "\")"; + }); + + py::enum_(iFileTreeClass, "InsertPolicy") + .value("FAIL_IF_EXISTS", IFileTree::InsertPolicy::FAIL_IF_EXISTS) + .value("REPLACE", IFileTree::InsertPolicy::REPLACE) + .value("MERGE", IFileTree::InsertPolicy::MERGE) + .export_values(); + + py::enum_(iFileTreeClass, "WalkReturn") + .value("CONTINUE", IFileTree::WalkReturn::CONTINUE) + .value("STOP", IFileTree::WalkReturn::STOP) + .value("SKIP", IFileTree::WalkReturn::SKIP) + .export_values(); + + // in C++ this is not an inner enum due to the conditional feature of glob(), + // but in Python this makes more sense as a inner enum + py::enum_(iFileTreeClass, "GlobPatternType") + .value("GLOB", GlobPatternType::GLOB) + .value("REGEX", GlobPatternType::REGEX) + .export_values(); + + // Non-mutable operations: + iFileTreeClass.def("exists", + py::overload_cast( + &IFileTree::exists, py::const_), + py::arg("path"), py::arg("type") = FILE_OR_DIRECTORY); + iFileTreeClass.def( + "find", py::overload_cast(&IFileTree::find), + py::arg("path"), py::arg("type") = FILE_OR_DIRECTORY); + iFileTreeClass.def("pathTo", &IFileTree::pathTo, py::arg("entry"), + py::arg("sep") = "\\"); + + iFileTreeClass.def( + "walk", + py::overload_cast< + std::function)>, + QString>(&IFileTree::walk, py::const_), + py::arg("callback"), py::arg("sep") = "\\"); + + // the walk() and glob() generator version are free functions in C++ due to the + // conditional nature, but in Python, it makes more sense to have them as method + // of IFileTree directly + + iFileTreeClass.def("walk", [](std::shared_ptr tree) { + return make_generator(walk(tree)); + }); + + iFileTreeClass.def( + "glob", + [](std::shared_ptr tree, QString pattern, + GlobPatternType patternType) { + return make_generator(glob(tree, pattern, patternType)); + }, + py::arg("pattern"), py::arg("type") = GlobPatternType::GLOB); + + // Kind-of-static operations: + iFileTreeClass.def("createOrphanTree", &IFileTree::createOrphanTree, + py::arg("name") = ""); + + // addFile() and addDirectory throws exception instead of returning null + // pointer in order to have better traces. + iFileTreeClass.def( + "addFile", + [](IFileTree* w, QString path, bool replaceIfExists) { + auto result = w->addFile(path, replaceIfExists); + if (result == nullptr) { + throw std::logic_error("addFile failed"); + } + return result; + }, + py::arg("path"), py::arg("replace_if_exists") = false); + iFileTreeClass.def( + "addDirectory", + [](IFileTree* w, QString path) { + auto result = w->addDirectory(path); + if (result == nullptr) { + throw std::logic_error("addDirectory failed"); + } + return result; + }, + py::arg("path")); + + // Merge needs custom return types depending if the user wants overrides + // or not. A failure is translated into an exception for easier tracing + // and handling. + iFileTreeClass.def( + "merge", + [](IFileTree* p, std::shared_ptr other, bool returnOverwrites) + -> std::variant { + IFileTree::OverwritesType overwrites; + auto result = p->merge(other, returnOverwrites ? &overwrites : nullptr); + if (result == IFileTree::MERGE_FAILED) { + throw std::logic_error("merge failed"); + } + if (returnOverwrites) { + return {overwrites}; + } + return {result}; + }, + py::arg("other"), py::arg("overwrites") = false); + + // Insert and erase returns an iterator, which makes no sense in python, + // so we convert it to bool. Erase is also renamed "remove" since + // "erase" is very C++. + iFileTreeClass.def( + "insert", + [](IFileTree* p, std::shared_ptr entry, + IFileTree::InsertPolicy insertPolicy) { + return p->insert(entry, insertPolicy) == p->end(); + }, + py::arg("entry"), + py::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS); + + iFileTreeClass.def( + "remove", + [](IFileTree* p, QString name) { + return p->erase(name).first != p->end(); + }, + py::arg("name")); + iFileTreeClass.def( + "remove", + [](IFileTree* p, std::shared_ptr entry) { + return p->erase(entry) != p->end(); + }, + py::arg("entry")); + + iFileTreeClass.def("move", &IFileTree::move, py::arg("entry"), py::arg("path"), + py::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS); + iFileTreeClass.def( + "copy", + [](IFileTree* w, std::shared_ptr entry, QString path, + IFileTree::InsertPolicy insertPolicy) { + auto result = w->copy(entry, path, insertPolicy); + if (result == nullptr) { + throw std::logic_error("copy failed"); + } + return result; + }, + py::arg("entry"), py::arg("path") = "", + py::arg("insert_policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS); + + iFileTreeClass.def("clear", &IFileTree::clear); + iFileTreeClass.def("removeAll", &IFileTree::removeAll, py::arg("names")); + iFileTreeClass.def("removeIf", &IFileTree::removeIf, py::arg("filter")); + + // Special methods: + iFileTreeClass.def("__getitem__", + py::overload_cast(&IFileTree::at)); + + iFileTreeClass.def("__iter__", [](IFileTree* tree) { + return py::make_iterator(*tree); + }); + iFileTreeClass.def("__len__", &IFileTree::size); + iFileTreeClass.def( + "__bool__", +[](const IFileTree* tree) { + return !tree->empty(); + }); + iFileTreeClass.def( + "__repr__", +[](const IFileTree* entry) { + return "IFileTree(\"" + entry->name() + "\")"; + }); + } + + void add_make_tree_function(pybind11::module_& m) + { + m.def( + "makeTree", + [](mo2::detail::PyFileTree::callback_t callback) + -> std::shared_ptr { + return std::make_shared(nullptr, "", callback); + }, + py::arg("callback") = mo2::detail::PyFileTree::callback_t{}); + } + +} // namespace mo2::python + +#pragma optimize("", on) diff --git a/libs/plugin_python/src/mobase/wrappers/pyfiletree.h b/libs/plugin_python/src/mobase/wrappers/pyfiletree.h new file mode 100644 index 0000000..0e94665 --- /dev/null +++ b/libs/plugin_python/src/mobase/wrappers/pyfiletree.h @@ -0,0 +1,34 @@ +#ifndef MO2_PYTHON_FILETREE_H +#define MO2_PYTHON_FILETREE_H + +#include "../pybind11_all.h" + +#include + +namespace pybind11 { + template <> + struct polymorphic_type_hook { + static const void* get(const MOBase::FileTreeEntry* src, + const std::type_info*& type); + }; +} // namespace pybind11 + +namespace mo2::python { + + /** + * @brief Add bindings for FileTreeEntry andIFileTree to the given module. + * + * @param mobase Module to add the bindings to. + */ + void add_ifiletree_bindings(pybind11::module_& m); + + /** + * @brief Add makeTree() function to the given module, useful for debugging. + * + * @param mobase Module to add the function to. + */ + void add_make_tree_function(pybind11::module_& m); + +} // namespace mo2::python + +#endif diff --git a/libs/plugin_python/src/mobase/wrappers/pyplugins.cpp b/libs/plugin_python/src/mobase/wrappers/pyplugins.cpp new file mode 100644 index 0000000..bcce0df --- /dev/null +++ b/libs/plugin_python/src/mobase/wrappers/pyplugins.cpp @@ -0,0 +1,262 @@ +#include "wrappers.h" + +#include + +#include "pyplugins.h" + +namespace py = pybind11; +using namespace pybind11::literals; +using namespace MOBase; + +namespace mo2::python { + + // this one is kind of big so it has its own function + void add_iplugingame_bindings(pybind11::module_ m) + { + py::enum_(m, "LoadOrderMechanism") + .value("None", IPluginGame::LoadOrderMechanism::None) + .value("FileTime", IPluginGame::LoadOrderMechanism::FileTime) + .value("PluginsTxt", IPluginGame::LoadOrderMechanism::PluginsTxt) + + .value("NONE", IPluginGame::LoadOrderMechanism::None) + .value("FILE_TIME", IPluginGame::LoadOrderMechanism::FileTime) + .value("PLUGINS_TXT", IPluginGame::LoadOrderMechanism::PluginsTxt); + + py::enum_(m, "SortMechanism") + .value("NONE", IPluginGame::SortMechanism::NONE) + .value("MLOX", IPluginGame::SortMechanism::MLOX) + .value("BOSS", IPluginGame::SortMechanism::BOSS) + .value("LOOT", IPluginGame::SortMechanism::LOOT); + + // this does not actually do the conversion, but might be convenient + // for accessing the names for enum bits + py::enum_(m, "ProfileSetting", py::arithmetic()) + .value("mods", IPluginGame::MODS) + .value("configuration", IPluginGame::CONFIGURATION) + .value("savegames", IPluginGame::SAVEGAMES) + .value("preferDefaults", IPluginGame::PREFER_DEFAULTS) + + .value("MODS", IPluginGame::MODS) + .value("CONFIGURATION", IPluginGame::CONFIGURATION) + .value("SAVEGAMES", IPluginGame::SAVEGAMES) + .value("PREFER_DEFAULTS", IPluginGame::PREFER_DEFAULTS); + + py::class_>( + m, "IPluginGame", py::multiple_inheritance()) + .def(py::init<>()) + .def("detectGame", &IPluginGame::detectGame) + .def("gameName", &IPluginGame::gameName) + .def("displayGameName", &IPluginGame::displayGameName) + .def("initializeProfile", &IPluginGame::initializeProfile, "directory"_a, + "settings"_a) + .def("listSaves", &IPluginGame::listSaves, "folder"_a) + .def("isInstalled", &IPluginGame::isInstalled) + .def("gameIcon", &IPluginGame::gameIcon) + .def("gameDirectory", &IPluginGame::gameDirectory) + .def("dataDirectory", &IPluginGame::dataDirectory) + .def("modDataDirectory", &IPluginGame::modDataDirectory) + .def("secondaryDataDirectories", &IPluginGame::secondaryDataDirectories) + .def("setGamePath", &IPluginGame::setGamePath, "path"_a) + .def("documentsDirectory", &IPluginGame::documentsDirectory) + .def("savesDirectory", &IPluginGame::savesDirectory) + .def("executables", &IPluginGame::executables) + .def("executableForcedLoads", &IPluginGame::executableForcedLoads) + .def("steamAPPId", &IPluginGame::steamAPPId) + .def("primaryPlugins", &IPluginGame::primaryPlugins) + .def("enabledPlugins", &IPluginGame::enabledPlugins) + .def("gameVariants", &IPluginGame::gameVariants) + .def("setGameVariant", &IPluginGame::setGameVariant, "variant"_a) + .def("binaryName", &IPluginGame::binaryName) + .def("gameShortName", &IPluginGame::gameShortName) + .def("lootGameName", &IPluginGame::lootGameName) + .def("primarySources", &IPluginGame::primarySources) + .def("validShortNames", &IPluginGame::validShortNames) + .def("gameNexusName", &IPluginGame::gameNexusName) + .def("iniFiles", &IPluginGame::iniFiles) + .def("DLCPlugins", &IPluginGame::DLCPlugins) + .def("CCPlugins", &IPluginGame::CCPlugins) + .def("loadOrderMechanism", &IPluginGame::loadOrderMechanism) + .def("sortMechanism", &IPluginGame::sortMechanism) + .def("nexusModOrganizerID", &IPluginGame::nexusModOrganizerID) + .def("nexusGameID", &IPluginGame::nexusGameID) + .def("looksValid", &IPluginGame::looksValid, "directory"_a) + .def("gameVersion", &IPluginGame::gameVersion) + .def("getLauncherName", &IPluginGame::getLauncherName) + .def("getSupportURL", &IPluginGame::getSupportURL) + .def("getModMappings", &IPluginGame::getModMappings); + } + + // multiple installers + void add_iplugininstaller_bindings(pybind11::module_ m) + { + // this is bind but should not be inherited in Python - does not make sense, + // having it makes it simpler to bind the Simple and Custom installers + py::class_, IPlugin, + std::unique_ptr>( + m, "IPluginInstaller", py::multiple_inheritance()) + .def("isArchiveSupported", &IPluginInstaller::isArchiveSupported, "tree"_a) + .def("priority", &IPluginInstaller::priority) + .def("onInstallationStart", &IPluginInstaller::onInstallationStart, + "archive"_a, "reinstallation"_a, "current_mod"_a) + .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, "result"_a, + "new_mod"_a) + .def("isManualInstaller", &IPluginInstaller::isManualInstaller) + .def("setParentWidget", &IPluginInstaller::setParentWidget, "parent"_a) + .def("setInstallationManager", &IPluginInstaller::setInstallationManager, + "manager"_a) + .def("_parentWidget", + &PyPluginInstallerBase::parentWidget) + .def("_manager", &PyPluginInstallerBase::manager, + py::return_value_policy::reference); + + py::class_>( + m, "IPluginInstallerSimple", py::multiple_inheritance()) + .def(py::init<>()) + + // note: keeping the variant here even if we always return a tuple + // to be consistent with the wrapper and have proper stubs generation. + .def( + "install", + [](IPluginInstallerSimple* p, GuessedValue& modName, + std::shared_ptr& tree, QString& version, + int& nexusID) -> PyPluginInstallerSimple::py_install_return_type { + auto result = p->install(modName, tree, version, nexusID); + return std::make_tuple(result, tree, version, nexusID); + }, + "name"_a, "tree"_a, "version"_a, "nexus_id"_a); + + py::class_>( + m, "IPluginInstallerCustom", py::multiple_inheritance()) + .def(py::init<>()) + .def("isArchiveSupported", &IPluginInstallerCustom::isArchiveSupported, + "archive_name"_a) + .def("supportedExtensions", &IPluginInstallerCustom::supportedExtensions) + .def("install", &IPluginInstallerCustom::install, "mod_name"_a, + "game_name"_a, "archive_name"_a, "version"_a, "nexus_id"_a); + } + + void add_plugins_bindings(pybind11::module_ m) + { + py::class_>( + m, "IPluginBase", py::multiple_inheritance()) + .def(py::init<>()) + .def("init", &IPlugin::init, "organizer"_a) + .def("name", &IPlugin::name) + .def("localizedName", &IPlugin::localizedName) + .def("master", &IPlugin::master) + .def("author", &IPlugin::author) + .def("description", &IPlugin::description) + .def("version", &IPlugin::version) + .def("requirements", &IPlugin::requirements) + .def("settings", &IPlugin::settings) + .def("enabledByDefault", &IPlugin::enabledByDefault); + + py::class_>(m, "IPlugin", + py::multiple_inheritance()) + .def(py::init<>()); + + py::class_>( + m, "IPluginFileMapper", py::multiple_inheritance()) + .def(py::init<>()) + .def("mappings", &IPluginFileMapper::mappings); + + py::class_>( + m, "IPluginDiagnose", py::multiple_inheritance()) + .def(py::init<>()) + .def("activeProblems", &IPluginDiagnose::activeProblems) + .def("shortDescription", &IPluginDiagnose::shortDescription, "key"_a) + .def("fullDescription", &IPluginDiagnose::fullDescription, "key"_a) + .def("hasGuidedFix", &IPluginDiagnose::hasGuidedFix, "key"_a) + .def("startGuidedFix", &IPluginDiagnose::startGuidedFix, "key"_a) + .def("_invalidate", &PyPluginDiagnose::invalidate); + + py::class_>( + m, "IPluginTool", py::multiple_inheritance()) + .def(py::init<>()) + .def("displayName", &IPluginTool::displayName) + .def("tooltip", &IPluginTool::tooltip) + .def("icon", &IPluginTool::icon) + .def("display", &IPluginTool::display) + .def("setParentWidget", &IPluginTool::setParentWidget) + .def("_parentWidget", &PyPluginTool::parentWidget); + + py::class_>( + m, "IPluginPreview", py::multiple_inheritance()) + .def(py::init<>()) + .def("supportedExtensions", &IPluginPreview::supportedExtensions) + .def("supportsArchives", &IPluginPreview::supportsArchives) + .def("genFilePreview", &IPluginPreview::genFilePreview, "filename"_a, + "max_size"_a) + .def("genDataPreview", &IPluginPreview::genDataPreview, "file_data"_a, + "filename"_a, "max_size"_a); + + py::class_>( + m, "IPluginModPage", py::multiple_inheritance()) + .def(py::init<>()) + .def("displayName", &IPluginModPage::displayName) + .def("icon", &IPluginModPage::icon) + .def("pageURL", &IPluginModPage::pageURL) + .def("useIntegratedBrowser", &IPluginModPage::useIntegratedBrowser) + .def("handlesDownload", &IPluginModPage::handlesDownload, "page_url"_a, + "download_url"_a, "fileinfo"_a) + .def("setParentWidget", &IPluginModPage::setParentWidget, "parent"_a) + .def("_parentWidget", &PyPluginModPage::parentWidget); + + add_iplugingame_bindings(m); + add_iplugininstaller_bindings(m); + } + + struct extract_plugins_helper { + QList objects; + + template + void append_if_instance(pybind11::object plugin_obj) + { + if (py::isinstance(plugin_obj)) { + objects.append(plugin_obj.cast()); + } + } + }; + + QList extract_plugins(pybind11::object plugin_obj) + { + extract_plugins_helper helper; + + // we need to check the trampoline class for these since the interfaces do not + // extend IPlugin + helper.append_if_instance(plugin_obj); + helper.append_if_instance(plugin_obj); + + helper.append_if_instance(plugin_obj); + helper.append_if_instance(plugin_obj); + helper.append_if_instance(plugin_obj); + + helper.append_if_instance(plugin_obj); + + // we need to check the two installer types because IPluginInstaller does not + // inherit QObject, and the trampoline do not have a common ancestor + helper.append_if_instance(plugin_obj); + helper.append_if_instance(plugin_obj); + + if (helper.objects.isEmpty()) { + helper.append_if_instance(plugin_obj); + } + + // tie the lifetime of the Python object to the lifetime of the QObject + for (auto* object : helper.objects) { + py::qt::set_qt_owner(object, plugin_obj); + } + + return helper.objects; + } + +} // namespace mo2::python diff --git a/libs/plugin_python/src/mobase/wrappers/pyplugins.h b/libs/plugin_python/src/mobase/wrappers/pyplugins.h new file mode 100644 index 0000000..17f7ab5 --- /dev/null +++ b/libs/plugin_python/src/mobase/wrappers/pyplugins.h @@ -0,0 +1,527 @@ +#ifndef PYTHON_WRAPPERS_PYPLUGINS_H +#define PYTHON_WRAPPERS_PYPLUGINS_H + +#include "../pybind11_all.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// these needs to be defined in a header file for automoc - this file is included only +// in pyplugins.cpp +namespace mo2::python { + + using namespace MOBase; + + // we need two base trampoline because IPluginGame has some final methods. + template + class PyPluginBaseNoFinal : public PluginBase { + public: + using PluginBase::PluginBase; + + PyPluginBaseNoFinal(PyPluginBaseNoFinal const&) = delete; + PyPluginBaseNoFinal(PyPluginBaseNoFinal&&) = delete; + PyPluginBaseNoFinal& operator=(PyPluginBaseNoFinal const&) = delete; + PyPluginBaseNoFinal& operator=(PyPluginBaseNoFinal&&) = delete; + + bool init(IOrganizer* organizer) override + { + PYBIND11_OVERRIDE_PURE(bool, PluginBase, init, organizer); + } + QString name() const override + { + PYBIND11_OVERRIDE_PURE(QString, PluginBase, name, ); + } + QString localizedName() const override + { + PYBIND11_OVERRIDE(QString, PluginBase, localizedName, ); + } + QString master() const override + { + PYBIND11_OVERRIDE(QString, PluginBase, master, ); + } + QString author() const override + { + PYBIND11_OVERRIDE_PURE(QString, PluginBase, author, ); + } + QString description() const override + { + PYBIND11_OVERRIDE_PURE(QString, PluginBase, description, ); + } + VersionInfo version() const override + { + PYBIND11_OVERRIDE_PURE(VersionInfo, PluginBase, version, ); + } + QList settings() const override + { + PYBIND11_OVERRIDE_PURE(QList, PluginBase, settings, ); + } + }; + + template + class PyPluginBase : public PyPluginBaseNoFinal { + public: + using PyPluginBaseNoFinal::PyPluginBaseNoFinal; + + std::vector> requirements() const + { + PYBIND11_OVERRIDE(std::vector>, + PluginBase, requirements, ); + } + bool enabledByDefault() const override + { + PYBIND11_OVERRIDE(bool, PluginBase, enabledByDefault, ); + } + }; + + // these classes do not inherit IPlugin or QObject so we need intermediate class to + // get proper bindings + class IPyPlugin : public QObject, public IPlugin {}; + class IPyPluginFileMapper : public IPyPlugin, public IPluginFileMapper {}; + class IPyPluginDiagnose : public IPyPlugin, public IPluginDiagnose {}; + + // PyXXX classes - trampoline classes for the plugins + + class PyPlugin : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin) + }; + + class PyPluginFileMapper : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginFileMapper) + public: + MappingType mappings() const override + { + PYBIND11_OVERRIDE_PURE(MappingType, IPyPluginFileMapper, mappings, ); + } + }; + + class PyPluginDiagnose : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginDiagnose) + public: + std::vector activeProblems() const + { + PYBIND11_OVERRIDE_PURE(std::vector, IPyPluginDiagnose, + activeProblems, ); + } + + QString shortDescription(unsigned int key) const + { + PYBIND11_OVERRIDE_PURE(QString, IPyPluginDiagnose, shortDescription, key); + } + + QString fullDescription(unsigned int key) const + { + PYBIND11_OVERRIDE_PURE(QString, IPyPluginDiagnose, fullDescription, key); + } + + bool hasGuidedFix(unsigned int key) const + { + PYBIND11_OVERRIDE_PURE(bool, IPyPluginDiagnose, hasGuidedFix, key); + } + + void startGuidedFix(unsigned int key) const + { + PYBIND11_OVERRIDE_PURE(void, IPyPluginDiagnose, startGuidedFix, key); + } + + // we need to bring this in public scope + using IPluginDiagnose::invalidate; + }; + + class PyPluginTool : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool) + public: + QString displayName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginTool, displayName, ); + } + QString tooltip() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginTool, tooltip, ); + } + QIcon icon() const override + { + PYBIND11_OVERRIDE_PURE(QIcon, IPluginTool, icon, ); + } + void setParentWidget(QWidget* widget) override + { + PYBIND11_OVERRIDE(void, IPluginTool, setParentWidget, widget); + } + void display() const override + { + PYBIND11_OVERRIDE_PURE(void, IPluginTool, display, ); + } + + // we need to bring this in public scope + using IPluginTool::parentWidget; + }; + + class PyPluginPreview : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginPreview) + public: + std::set supportedExtensions() const override + { + PYBIND11_OVERRIDE_PURE(std::set, IPluginPreview, + supportedExtensions, ); + } + + bool supportsArchives() const override + { + PYBIND11_OVERRIDE(bool, IPluginPreview, supportsArchives, ); + } + + QWidget* genFilePreview(const QString& fileName, + const QSize& maxSize) const override + { + PYBIND11_OVERRIDE_PURE(QWidget*, IPluginPreview, genFilePreview, fileName, + maxSize); + } + + QWidget* genDataPreview(const QByteArray& fileData, const QString& fileName, + const QSize& maxSize) const override + { + PYBIND11_OVERRIDE(QWidget*, IPluginPreview, genDataPreview, fileData, + fileName, maxSize); + } + }; + + class PyPluginModPage : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginModPage) + public: + QString displayName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginModPage, displayName, ); + } + + QIcon icon() const override + { + PYBIND11_OVERRIDE_PURE(QIcon, IPluginModPage, icon, ); + } + + QUrl pageURL() const override + { + PYBIND11_OVERRIDE_PURE(QUrl, IPluginModPage, pageURL, ); + } + + bool useIntegratedBrowser() const override + { + PYBIND11_OVERRIDE_PURE(bool, IPluginModPage, useIntegratedBrowser, ); + } + + bool handlesDownload(const QUrl& pageURL, const QUrl& downloadURL, + ModRepositoryFileInfo& fileInfo) const override + { + // TODO: cannot modify fileInfo from Python + PYBIND11_OVERRIDE_PURE(bool, IPluginModPage, handlesDownload, pageURL, + downloadURL, &fileInfo); + } + + void setParentWidget(QWidget* widget) override + { + PYBIND11_OVERRIDE(void, IPluginModPage, setParentWidget, widget); + } + + // we need to bring this in public scope + using IPluginModPage::parentWidget; + }; + + // installers + template + class PyPluginInstallerBase : public PyPluginBase { + public: + using PyPluginBase::PyPluginBase; + + unsigned int priority() const override + { + PYBIND11_OVERRIDE_PURE(unsigned int, PluginInstallerBase, priority); + } + + bool isManualInstaller() const override + { + PYBIND11_OVERRIDE_PURE(bool, PluginInstallerBase, isManualInstaller, ); + } + + void onInstallationStart(QString const& archive, bool reinstallation, + IModInterface* currentMod) + { + PYBIND11_OVERRIDE(void, PluginInstallerBase, onInstallationStart, archive, + reinstallation, currentMod); + } + + void onInstallationEnd(IPluginInstaller::EInstallResult result, + IModInterface* newMod) + { + PYBIND11_OVERRIDE(void, PluginInstallerBase, onInstallationEnd, result, + newMod); + } + + bool isArchiveSupported(std::shared_ptr tree) const override + { + PYBIND11_OVERRIDE_PURE(bool, PluginInstallerBase, isArchiveSupported, tree); + } + + // we need to bring these in public scope + using PluginInstallerBase::manager; + using PluginInstallerBase::parentWidget; + }; + + class PyPluginInstallerCustom + : public PyPluginInstallerBase { + Q_OBJECT + Q_INTERFACES( + MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerCustom) + public: + bool isArchiveSupported(const QString& archiveName) const + { + PYBIND11_OVERRIDE_PURE(bool, IPluginInstallerCustom, isArchiveSupported, + archiveName); + } + + std::set supportedExtensions() const + { + PYBIND11_OVERRIDE_PURE(std::set, IPluginInstallerCustom, + supportedExtensions, ); + } + + EInstallResult install(GuessedValue& modName, QString gameName, + const QString& archiveName, const QString& version, + int nexusID) override + { + PYBIND11_OVERRIDE_PURE(EInstallResult, IPluginInstallerCustom, install, + &modName, gameName, archiveName, version, nexusID); + } + }; + + class PyPluginInstallerSimple + : public PyPluginInstallerBase { + Q_OBJECT + Q_INTERFACES( + MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) + public: + using py_install_return_type = + std::variant, + std::tuple, QString, int>>; + + EInstallResult install(GuessedValue& modName, + std::shared_ptr& tree, QString& version, + int& nexusID) override + { + const auto result = [&, this]() { + PYBIND11_OVERRIDE_PURE(py_install_return_type, IPluginInstallerSimple, + install, &modName, tree, version, nexusID); + }(); + + return std::visit( + [&tree, &version, &nexusID](auto const& t) { + using type = std::decay_t; + if constexpr (std::is_same_v) { + return t; + } + else if constexpr (std::is_same_v>) { + tree = t; + return RESULT_SUCCESS; + } + else if constexpr (std::is_same_v< + type, std::tuple, + QString, int>>) { + tree = std::get<1>(t); + version = std::get<2>(t); + nexusID = std::get<3>(t); + return std::get<0>(t); + } + }, + result); + } + }; + + // game + class PyPluginGame : public PyPluginBaseNoFinal { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame) + public: + void detectGame() override + { + PYBIND11_OVERRIDE_PURE(void, IPluginGame, detectGame, ); + } + QString gameName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameName, ); + } + QString displayGameName() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, displayGameName, ); + } + void initializeProfile(const QDir& directory, + ProfileSettings settings) const override + { + PYBIND11_OVERRIDE_PURE(void, IPluginGame, initializeProfile, directory, + settings); + } + std::vector> + listSaves(QDir folder) const override + { + PYBIND11_OVERRIDE_PURE(std::vector>, + IPluginGame, listSaves, folder); + } + bool isInstalled() const override + { + PYBIND11_OVERRIDE_PURE(bool, IPluginGame, isInstalled, ); + } + QIcon gameIcon() const override + { + PYBIND11_OVERRIDE_PURE(QIcon, IPluginGame, gameIcon, ); + } + QDir gameDirectory() const override + { + PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, gameDirectory, ); + } + QDir dataDirectory() const override + { + PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, dataDirectory, ); + } + QString modDataDirectory() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, modDataDirectory, ); + } + QMap secondaryDataDirectories() const override + { + using string_dir_map = QMap; + PYBIND11_OVERRIDE(string_dir_map, IPluginGame, secondaryDataDirectories, ); + } + void setGamePath(const QString& path) override + { + PYBIND11_OVERRIDE_PURE(void, IPluginGame, setGamePath, path); + } + QDir documentsDirectory() const override + { + PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, documentsDirectory, ); + } + QDir savesDirectory() const override + { + PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, savesDirectory, ); + } + QList executables() const override + { + PYBIND11_OVERRIDE(QList, IPluginGame, executables, ); + } + QList executableForcedLoads() const override + { + PYBIND11_OVERRIDE_PURE(QList, IPluginGame, + executableForcedLoads, ); + } + QString steamAPPId() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, steamAPPId, ); + } + QStringList primaryPlugins() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, primaryPlugins, ); + } + QStringList enabledPlugins() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, enabledPlugins, ); + } + QStringList gameVariants() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, gameVariants, ); + } + void setGameVariant(const QString& variant) override + { + PYBIND11_OVERRIDE_PURE(void, IPluginGame, setGameVariant, variant); + } + QString binaryName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginGame, binaryName, ); + } + QString gameShortName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameShortName, ); + } + QString lootGameName() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, lootGameName, ); + } + QStringList primarySources() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, primarySources, ); + } + QStringList validShortNames() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, validShortNames, ); + } + QString gameNexusName() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, gameNexusName, ); + } + QStringList iniFiles() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, iniFiles, ); + } + QStringList DLCPlugins() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, DLCPlugins, ); + } + QStringList CCPlugins() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, CCPlugins, ); + } + LoadOrderMechanism loadOrderMechanism() const override + { + PYBIND11_OVERRIDE(LoadOrderMechanism, IPluginGame, loadOrderMechanism, ); + } + SortMechanism sortMechanism() const override + { + PYBIND11_OVERRIDE(SortMechanism, IPluginGame, sortMechanism, ); + } + int nexusModOrganizerID() const override + { + PYBIND11_OVERRIDE(int, IPluginGame, nexusModOrganizerID, ); + } + int nexusGameID() const override + { + PYBIND11_OVERRIDE_PURE(int, IPluginGame, nexusGameID, ); + } + bool looksValid(QDir const& dir) const override + { + PYBIND11_OVERRIDE_PURE(bool, IPluginGame, looksValid, dir); + } + QString gameVersion() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameVersion, ); + } + QString getLauncherName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginGame, getLauncherName, ); + } + QString getSupportURL() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, getSupportURL, ); + } + QMap getModMappings() const override + { + using vfs_map = QMap; + PYBIND11_OVERRIDE(vfs_map, IPluginGame, getModMappings, ); + } + }; + +} // namespace mo2::python + +#endif diff --git a/libs/plugin_python/src/mobase/wrappers/utils.cpp b/libs/plugin_python/src/mobase/wrappers/utils.cpp new file mode 100644 index 0000000..95111fa --- /dev/null +++ b/libs/plugin_python/src/mobase/wrappers/utils.cpp @@ -0,0 +1,50 @@ +#include "wrappers.h" + +#include "../pybind11_all.h" + +#include +#include + +#ifdef _WIN32 +#include "known_folders.h" +#endif + +namespace py = pybind11; +using namespace MOBase; + +namespace mo2::python { + + void add_utils_bindings(pybind11::module_ m) + { +#ifdef _WIN32 + py::class_ pyKnownFolder(m, "KnownFolder"); + for (std::size_t i = 0; i < KNOWN_FOLDERS.size(); ++i) { + pyKnownFolder.attr(KNOWN_FOLDERS[i].name) = py::int_(i); + } + + m.def( + "getKnownFolder", + [](std::size_t knownFolderId, QString what) { + return getKnownFolder(KNOWN_FOLDERS.at(knownFolderId).guid, what); + }, + py::arg("known_folder"), py::arg("what") = ""); + + m.def( + "getOptionalKnownFolder", + [](std::size_t knownFolderId) { + const auto r = + getOptionalKnownFolder(KNOWN_FOLDERS.at(knownFolderId).guid); + return r.isEmpty() ? py::none{} : py::cast(r); + }, + py::arg("known_folder")); +#endif + + m.def("getFileVersion", wrap_for_filepath(&MOBase::getFileVersion), + py::arg("filepath")); + m.def("getProductVersion", wrap_for_filepath(&MOBase::getProductVersion), + py::arg("executable")); + m.def("getIconForExecutable", wrap_for_filepath(&MOBase::iconForExecutable), + py::arg("executable")); + } + +} // namespace mo2::python diff --git a/libs/plugin_python/src/mobase/wrappers/widgets.cpp b/libs/plugin_python/src/mobase/wrappers/widgets.cpp new file mode 100644 index 0000000..e626540 --- /dev/null +++ b/libs/plugin_python/src/mobase/wrappers/widgets.cpp @@ -0,0 +1,77 @@ +#include "wrappers.h" + +#include "../pybind11_all.h" + +#include + +namespace py = pybind11; +using namespace MOBase; + +namespace mo2::python { + + void add_widget_bindings(pybind11::module_ m) + { + // TaskDialog is also in Windows System. + using TaskDialog = MOBase::TaskDialog; + + // TaskDialog + py::class_(m, "TaskDialogButton") + .def(py::init(), + py::arg("text"), py::arg("description"), py::arg("button")) + .def(py::init(), py::arg("text"), + py::arg("button")) + .def_readwrite("text", &TaskDialogButton::text) + .def_readwrite("description", &TaskDialogButton::description) + .def_readwrite("button", &TaskDialogButton::button); + + py::class_(m, "TaskDialog") + .def(py::init([](QWidget* parent, QString const& title, QString const& main, + QString const& content, QString const& details, + QMessageBox::Icon icon, + std::vector const& buttons, + std::variant> const& + remember) { + auto* dialog = new TaskDialog(parent, title); + dialog->main(main).content(content).details(details).icon(icon); + + for (auto& button : buttons) { + dialog->button(button); + } + + std::visit( + [dialog](auto const& item) { + QString action, file; + if constexpr (std::is_same_v, + QString>) { + action = item; + } + else { + action = std::get<0>(item); + file = std::get<1>(item); + } + dialog->remember(action, file); + }, + remember); + + return dialog; + }), + py::return_value_policy::take_ownership, + py::arg("parent") = static_cast(nullptr), + py::arg("title") = "", py::arg("main") = "", py::arg("content") = "", + py::arg("details") = "", py::arg("icon") = QMessageBox::NoIcon, + py::arg("buttons") = std::vector{}, + py::arg("remember") = "") + .def("setTitle", &TaskDialog::title, py::arg("title")) + .def("setMain", &TaskDialog::main, py::arg("main")) + .def("setContent", &TaskDialog::content, py::arg("content")) + .def("setDetails", &TaskDialog::details, py::arg("details")) + .def("setIcon", &TaskDialog::icon, py::arg("icon")) + .def("addButton", &TaskDialog::button, py::arg("button")) + .def("setRemember", &TaskDialog::remember, py::arg("action"), + py::arg("file") = "") + .def("setWidth", &TaskDialog::setWidth, py::arg("width")) + .def("addContent", &TaskDialog::addContent, py::arg("widget")) + .def("exec", &TaskDialog::exec); + } + +} // namespace mo2::python diff --git a/libs/plugin_python/src/mobase/wrappers/wrappers.cpp b/libs/plugin_python/src/mobase/wrappers/wrappers.cpp new file mode 100644 index 0000000..10f043d --- /dev/null +++ b/libs/plugin_python/src/mobase/wrappers/wrappers.cpp @@ -0,0 +1,120 @@ + +#include "wrappers.h" + +#include "../pybind11_all.h" + +#include +#include +#include +#include + +// IOrganizer must be bring here to properly compile the Python bindings of +// plugin requirements +#include +#include +#include +#include + +using namespace pybind11::literals; +namespace py = pybind11; +using namespace MOBase; + +namespace mo2::python { + + class PyPluginRequirement : public IPluginRequirement { + public: + std::optional check(IOrganizer* organizer) const override + { + PYBIND11_OVERRIDE_PURE(std::optional, IPluginRequirement, check, + organizer); + }; + }; + + class PySaveGame : public ISaveGame { + public: + QString getFilepath() const override + { + PYBIND11_OVERRIDE_PURE(FileWrapper, ISaveGame, getFilepath, ); + } + + QDateTime getCreationTime() const override + { + PYBIND11_OVERRIDE_PURE(QDateTime, ISaveGame, getCreationTime, ); + } + + QString getName() const override + { + PYBIND11_OVERRIDE_PURE(QString, ISaveGame, getName, ); + } + + QString getSaveGroupIdentifier() const override + { + PYBIND11_OVERRIDE_PURE(QString, ISaveGame, getSaveGroupIdentifier, ); + } + + QStringList allFiles() const override + { + return toQStringList([&] { + PYBIND11_OVERRIDE_PURE(QList, ISaveGame, allFiles, ); + }()); + } + + ~PySaveGame() { std::cout << "~PySaveGame()" << std::endl; } + }; + + class PySaveGameInfoWidget : public ISaveGameInfoWidget { + public: + // Bring the constructor: + using ISaveGameInfoWidget::ISaveGameInfoWidget; + + void setSave(ISaveGame const& save) override + { + PYBIND11_OVERRIDE_PURE(void, ISaveGameInfoWidget, setSave, &save); + } + + ~PySaveGameInfoWidget() { std::cout << "~PySaveGameInfoWidget()" << std::endl; } + }; + + void add_wrapper_bindings(pybind11::module_ m) + { + // ISaveGame - custom type_caster<> for shared_ptr<> to keep the Python object + // alive when returned from Python (see shared_cpp_owner.h) + + py::class_>(m, "ISaveGame") + .def(py::init<>()) + .def("getFilepath", wrap_return_for_filepath(&ISaveGame::getFilepath)) + .def("getCreationTime", &ISaveGame::getCreationTime) + .def("getName", &ISaveGame::getName) + .def("getSaveGroupIdentifier", &ISaveGame::getSaveGroupIdentifier) + .def("allFiles", [](ISaveGame* s) -> QList { + const auto result = s->allFiles(); + return {result.begin(), result.end()}; + }); + + // ISaveGameInfoWidget - custom holder to keep the Python object alive alongside + // the widget + + py::class_> + iSaveGameInfoWidget(m, "ISaveGameInfoWidget"); + iSaveGameInfoWidget.def(py::init(), "parent"_a = (QWidget*)nullptr) + .def("setSave", &ISaveGameInfoWidget::setSave, "save"_a); + py::qt::add_qt_delegate(iSaveGameInfoWidget, "_widget"); + + // IPluginRequirement - custom type_caster<> for shared_ptr<> to keep the Python + // object alive when returned from Python (see shared_cpp_owner.h) + + py::class_, + PyPluginRequirement> + iPluginRequirementClass(m, "IPluginRequirement"); + + py::class_(iPluginRequirementClass, "Problem") + .def(py::init(), "short_description"_a, + "long_description"_a = "") + .def("shortDescription", &IPluginRequirement::Problem::shortDescription) + .def("longDescription", &IPluginRequirement::Problem::longDescription); + + iPluginRequirementClass.def("check", &IPluginRequirement::check, "organizer"_a); + } + +} // namespace mo2::python diff --git a/libs/plugin_python/src/mobase/wrappers/wrappers.h b/libs/plugin_python/src/mobase/wrappers/wrappers.h new file mode 100644 index 0000000..43dd574 --- /dev/null +++ b/libs/plugin_python/src/mobase/wrappers/wrappers.h @@ -0,0 +1,109 @@ +#ifndef PYTHON_WRAPPERS_WRAPPERS_H +#define PYTHON_WRAPPERS_WRAPPERS_H + +#include +#include +#include + +#include + +#include +#include + +#include + +namespace mo2::python { + + /** + * @brief Add bindings for the various classes in uibase that are not + * wrappers (i.e., cannot be extended from Python). + * + * @param m Python module to add bindings to. + */ + void add_basic_bindings(pybind11::module_ m); + + /** + * @brief Add bindings for the various custom widget classes in uibase that + * cannot be extended from Python. + * + * @param m Python module to add bindings to. + */ + void add_widget_bindings(pybind11::module_ m); + + /** + * @brief Add bindings for the various utilities classes and functions in uibase + * that cannot be extended from Python. + * + * @param m Python module to add bindings to. + */ + void add_utils_bindings(pybind11::module_ m); + + /** + * @brief Add bindings for the uibase wrappers to the given module. uibase + * wrappers include classes from uibase that can be extended from Python but + * are neither plugins nor game features (e.g., ISaveGame). + * + * @param m Python module to add bindings to. + */ + void add_wrapper_bindings(pybind11::module_ m); + + /** + * @brief Add bindings for the various plugin classes in uibase that can be + * extended from Python. + * + * @param m Python module to add bindings to. + */ + void add_plugins_bindings(pybind11::module_ m); + + /** + * @brief Extract plugins from the given object. For each plugin implemented, an + * object is returned. + * + * The returned QObject* are set as owner of the given object so that the Python + * object lifetime does not end immediately after returning to C++. + * + * @param object Python object to extract plugins from. + * + * @return a QObject* for each plugin implemented by the given object. + */ + QList extract_plugins(pybind11::object object); + + /** + * @brief Add bindings for the various game features classes in uibase that + * can be extended from Python. + * + * @param m Python module to add bindings to. + */ + void add_game_feature_bindings(pybind11::module_ m); + + /** + * @brief Add bindings for IGameFeatures. + * + * @param m Python module to add bindings to. + */ + void add_igamefeatures_classes(pybind11::module_ m); + + /** + * @brief Extract the game feature corresponding to the given Python type. + * + * @param gameFeatures Game features to extract the feature from. + * @param type Type of the feature to extract. + * + * @return the feature from the game, or None is the game as no such feature. + */ + pybind11::object extract_feature(MOBase::IGameFeatures const& gameFeatures, + pybind11::object type); + + /** + * @brief Unregister the game feature corresponding to the given Python type. + * + * @param gameFeatures Game features to unregister the feature from. + * @param type Type of the feature to unregister. + * + * @return the feature from the game, or None is the game as no such feature. + */ + int unregister_feature(MOBase::IGameFeatures& gameFeatures, pybind11::object type); + +} // namespace mo2::python + +#endif // PYTHON_WRAPPERS_WRAPPERS_H diff --git a/libs/plugin_python/src/plugin_python_en.ts b/libs/plugin_python/src/plugin_python_en.ts new file mode 100644 index 0000000..9ef7836 --- /dev/null +++ b/libs/plugin_python/src/plugin_python_en.ts @@ -0,0 +1,83 @@ + + + + + ProxyPython + + + Python Initialization failed + + + + + On a previous start the Python Plugin failed to initialize. +Do you want to try initializing python again (at the risk of another crash)? + Suggestion: Select "no", and click the warning sign for further help.Afterwards you have to re-enable the python plugin. + + + + + Python Proxy + + + + + Proxy Plugin to allow plugins written in python to be loaded + + + + + ModOrganizer path contains a semicolon + + + + + Python DLL not found + + + + + Invalid Python DLL + + + + + Initializing Python failed + + + + + + invalid problem key %1 + + + + + The path to Mod Organizer (%1) contains a semicolon.<br>While this is legal on NTFS drives, many applications do not handle it correctly.<br>Unfortunately MO depends on libraries that seem to fall into that group.<br>As a result the python plugin cannot be loaded, and the only solution we can offer is to remove the semicolon or move MO to a path without a semicolon. + + + + + The Python plugin DLL was not found, maybe your antivirus deleted it. Re-installing MO2 might fix the problem. + + + + + The Python plugin DLL is invalid, maybe your antivirus is blocking it. Re-installing MO2 and adding exclusions for it to your AV might fix the problem. + + + + + The initialization of the Python plugin DLL failed, unfortunately without any details. + + + + + QObject + + + An unknown exception was thrown in python code. + + + + diff --git a/libs/plugin_python/src/proxy/CMakeLists.txt b/libs/plugin_python/src/proxy/CMakeLists.txt new file mode 100644 index 0000000..ed212a3 --- /dev/null +++ b/libs/plugin_python/src/proxy/CMakeLists.txt @@ -0,0 +1,118 @@ +cmake_minimum_required(VERSION 3.16) + +if(NOT TARGET mo2::uibase) + find_package(mo2-uibase CONFIG REQUIRED) +endif() + +set(PLUGIN_NAME "plugin_python") + +add_library(proxy SHARED proxypython.cpp proxypython.h) +mo2_configure_plugin(proxy + NO_SOURCES + WARNINGS 4 + EXTERNAL_WARNINGS 4 + TRANSLATIONS OFF + EXTRA_TRANSLATIONS + ${CMAKE_CURRENT_SOURCE_DIR}/../runner + ${CMAKE_CURRENT_SOURCE_DIR}/../mobase + ${CMAKE_CURRENT_SOURCE_DIR}/../pybind11-qt) +mo2_default_source_group() +target_link_libraries(proxy PRIVATE runner mo2::uibase) +if(NOT WIN32) + target_compile_definitions(proxy PRIVATE + MO2_PYTHON_PURELIB_DIR="${Python_PURELIB_DIR}" + MO2_PYTHON_PLATLIB_DIR="${Python_PLATLIB_DIR}") +endif() +set_target_properties(proxy PROPERTIES OUTPUT_NAME ${PLUGIN_NAME}) +mo2_install_plugin(proxy FOLDER) + +set(PLUGIN_PYTHON_DIR bin/plugins/${PLUGIN_NAME}) + +# install runner +if(WIN32) + target_link_options(proxy PRIVATE "/DELAYLOAD:runner.dll") + install(FILES $ DESTINATION ${PLUGIN_PYTHON_DIR}/dlls) +endif() + +# translations (custom location) +mo2_add_translations(proxy + TS_FILE ${CMAKE_CURRENT_SOURCE_DIR}/../${PLUGIN_NAME}_en.ts + SOURCES + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../runner + ${CMAKE_CURRENT_SOURCE_DIR}/../mobase + ${CMAKE_CURRENT_SOURCE_DIR}/../pybind11-qt) + +# install DLLs files needed +set(DLL_DIRS ${PLUGIN_PYTHON_DIR}/dlls) +if(WIN32) + file(GLOB dlls_to_install + ${Python_HOME}/dlls/libffi*.dll + ${Python_HOME}/dlls/sqlite*.dll + ${Python_HOME}/dlls/libssl*.dll + ${Python_HOME}/dlls/libcrypto*.dll + ${Python_HOME}/python${Python_VERSION_MAJOR}*.dll) + install(FILES ${dlls_to_install} DESTINATION ${DLL_DIRS}) +endif() + +# install Python .pyd files +set(PYLIB_DIR ${PLUGIN_PYTHON_DIR}/libs) +if(WIN32) + file(GLOB libs_to_install ${Python_DLL_DIR}/*.pyd) + install(FILES ${libs_to_install} DESTINATION ${PYLIB_DIR}) +endif() + +# generate + install standard library +if(WIN32) + set(pythoncore_zip "${CMAKE_CURRENT_BINARY_DIR}/pythoncore.zip") + add_custom_command( + TARGET proxy POST_BUILD + COMMAND ${Python_EXECUTABLE} + "${CMAKE_CURRENT_SOURCE_DIR}/build_pythoncore.py" + ${pythoncore_zip} + ) + install(FILES ${pythoncore_zip} DESTINATION ${PYLIB_DIR}) +endif() + +# install mobase +install(TARGETS mobase DESTINATION ${PYLIB_DIR}) + +# install PyQt6 +if(WIN32) + install( + DIRECTORY ${CMAKE_BINARY_DIR}/pylibs/PyQt${MO2_QT_VERSION_MAJOR} + DESTINATION ${PYLIB_DIR} + PATTERN "*.pyd" + PATTERN "*.pyi" + PATTERN "__pycache__" EXCLUDE + PATTERN "bindings" EXCLUDE + PATTERN "lupdate" EXCLUDE + PATTERN "Qt6" EXCLUDE + PATTERN "uic" EXCLUDE + ) +endif() + +if(NOT WIN32) + # Build-tree runtime layout expected by proxypython.cpp. + add_custom_command(TARGET proxy POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/src/src/plugins" + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/src/src/plugins/dlls" + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/src/src/plugins/libs" + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/src/src/plugins/plugin_python/dlls" + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/src/src/plugins/plugin_python/libs" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "${CMAKE_BINARY_DIR}/src/src/plugins/$" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "${CMAKE_BINARY_DIR}/src/src/plugins/dlls/$" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "${CMAKE_BINARY_DIR}/src/src/plugins/plugin_python/dlls/$" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "${CMAKE_BINARY_DIR}/src/src/plugins/libs/$" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "${CMAKE_BINARY_DIR}/src/src/plugins/plugin_python/libs/$") +endif() diff --git a/libs/plugin_python/src/proxy/build_pythoncore.py b/libs/plugin_python/src/proxy/build_pythoncore.py new file mode 100644 index 0000000..01c42f9 --- /dev/null +++ b/libs/plugin_python/src/proxy/build_pythoncore.py @@ -0,0 +1,15 @@ +import sys +import sysconfig +import zipfile +from pathlib import Path + +_EXCLUDE_MODULES = ["ensurepip", "idlelib", "test", "tkinter", "turtle_demo", "venv"] + +libdir = Path(sysconfig.get_path("stdlib")) +assert libdir.exists() + +with zipfile.PyZipFile(sys.argv[1], optimize=2, mode="w") as fp: + fp.writepy(libdir) # pyright: ignore[reportArgumentType] + for path in libdir.iterdir(): + if path.is_dir() and path.name not in _EXCLUDE_MODULES: + fp.writepy(path) # pyright: ignore[reportArgumentType] diff --git a/libs/plugin_python/src/proxy/proxypython.cpp b/libs/plugin_python/src/proxy/proxypython.cpp new file mode 100644 index 0000000..af897db --- /dev/null +++ b/libs/plugin_python/src/proxy/proxypython.cpp @@ -0,0 +1,368 @@ +/* +Copyright (C) 2022 Sebastian Herbord & MO2 Team. All rights reserved. + +This file is part of python proxy plugin for MO + +python proxy plugin is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Python proxy plugin is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with python proxy plugin. If not, see . +*/ +#include "proxypython.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace fs = std::filesystem; +using namespace MOBase; + +// retrieve the path to the folder containing the proxy DLL +fs::path getPluginFolder() +{ +#ifdef _WIN32 + wchar_t path[MAX_PATH]; + HMODULE hm = NULL; + + if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCWSTR)&getPluginFolder, &hm) == 0) { + return {}; + } + if (GetModuleFileName(hm, path, sizeof(path)) == 0) { + return {}; + } + + return fs::path(path).parent_path(); +#else + Dl_info info; + if (dladdr((void*)&getPluginFolder, &info) == 0 || info.dli_fname == nullptr) { + return {}; + } + return fs::path(info.dli_fname).parent_path(); +#endif +} + +ProxyPython::ProxyPython() + : m_MOInfo{nullptr}, m_RunnerLib{nullptr}, m_Runner{nullptr}, + m_LoadFailure(FailureType::NONE) +{ +} + +bool ProxyPython::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + + if (m_MOInfo && !m_MOInfo->isPluginEnabled(this)) { + return false; + } + + if (QCoreApplication::applicationDirPath().contains(';')) { + m_LoadFailure = FailureType::SEMICOLON; + return true; + } + + const auto pluginFolder = getPluginFolder(); +#ifdef _WIN32 + const auto pluginDataRoot = + fs::exists(pluginFolder / "plugin_python") ? (pluginFolder / "plugin_python") + : pluginFolder; +#else + // Linux portable installs can contain a legacy Windows "plugin_python" + // payload (pythoncore.zip/.pyd) that is incompatible with our bundled + // Linux runtime. Always use the plugin root on Linux. + const auto pluginDataRoot = pluginFolder; +#endif + + if (pluginFolder.empty()) { +#ifdef _WIN32 + DWORD error = ::GetLastError(); + m_LoadFailure = FailureType::DLL_NOT_FOUND; + log::error("failed to resolve Python proxy directory ({}): {}", error, + qUtf8Printable(windowsErrorString(::GetLastError()))); +#else + m_LoadFailure = FailureType::DLL_NOT_FOUND; + log::error("failed to resolve Python proxy directory: {}", + std::strerror(errno)); +#endif + return false; + } + + if (m_MOInfo && m_MOInfo->persistent(name(), "tryInit", false).toBool()) { + m_LoadFailure = FailureType::INITIALIZATION; + if (QMessageBox::question( + parentWidget(), tr("Python Initialization failed"), + tr("On a previous start the Python Plugin failed to initialize.\n" + "Do you want to try initializing python again (at the risk of " + "another crash)?\n " + "Suggestion: Select \"no\", and click the warning sign for further " + "help.Afterwards you have to re-enable the python plugin."), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) == QMessageBox::No) { + // we force enabled here (note: this is a persistent settings since MO2 2.4 + // or something), plugin + // usually should not handle enabled/disabled themselves but this is a base + // plugin so... + m_MOInfo->setPersistent(name(), "enabled", false, true); + return true; + } + } + + if (m_MOInfo) { + m_MOInfo->setPersistent(name(), "tryInit", true); + } + + // load the pythonrunner library, this is done in multiple steps: + // + // 1. we set the dlls/ subfolder (from the plugin) as the DLL directory so Windows + // will look for DLLs in it, this is required to find the Python and libffi DLL, but + // also the runner DLL + // + const auto dllPaths = pluginDataRoot / "dlls"; +#ifdef _WIN32 + if (SetDllDirectoryW(dllPaths.c_str()) == 0) { + DWORD error = ::GetLastError(); + m_LoadFailure = FailureType::DLL_NOT_FOUND; + log::error("failed to add python DLL directory ({}): {}", error, + qUtf8Printable(windowsErrorString(::GetLastError()))); + return false; + } +#endif + + // 2. we create the Python runner, we do not need to use ::LinkLibrary and + // ::GetProcAddress because we use delayed load for the runner DLL (see the + // CMakeLists.txt) + // + m_Runner = mo2::python::createPythonRunner(); + + if (m_Runner) { + const auto libpath = pluginDataRoot / "libs"; +#ifdef _WIN32 + const std::vector paths{ + libpath / "pythoncore.zip", libpath, + std::filesystem::path{IOrganizer::getPluginDataPath().toStdWString()}}; + m_Runner->initialize(paths); +#else + // On Linux, rely on the unpacked stdlib from PYTHONHOME (AppRun sets + // MO2_PYTHON_DIR/PYTHONHOME). Do not prepend pythoncore.zip here: + // forcing zipimport can fail when zlib is unavailable in embedded mode. + std::vector paths{ + libpath, + std::filesystem::path{IOrganizer::getPluginDataPath().toStdWString()}}; + + // Allow portable/AppImage builds to ship Python packages next to the app. + // MO2_PYTHON_DIR (set by AppRun) points to the writable python/ dir + // next to the AppImage; fall back to /python. + fs::path pythonDir; + const char* envPy = std::getenv("MO2_PYTHON_DIR"); + if (envPy && envPy[0] != '\0') { + pythonDir = fs::path(envPy); + } else { + pythonDir = fs::path(QCoreApplication::applicationDirPath().toStdWString()) / "python"; + } + paths.emplace_back(pythonDir / "site-packages"); + paths.emplace_back(pythonDir); + +#ifdef MO2_PYTHON_PURELIB_DIR + if (std::strlen(MO2_PYTHON_PURELIB_DIR) > 0) { + paths.emplace_back(MO2_PYTHON_PURELIB_DIR); + } +#endif +#ifdef MO2_PYTHON_PLATLIB_DIR + if (std::strlen(MO2_PYTHON_PLATLIB_DIR) > 0 && + std::strcmp(MO2_PYTHON_PLATLIB_DIR, MO2_PYTHON_PURELIB_DIR) != 0) { + paths.emplace_back(MO2_PYTHON_PLATLIB_DIR); + } +#endif + m_Runner->initialize(paths); +#endif + } + + if (m_MOInfo) { + m_MOInfo->setPersistent(name(), "tryInit", false); + } + + // reset DLL directory +#ifdef _WIN32 + SetDllDirectoryW(NULL); +#endif + + if (!m_Runner || !m_Runner->isInitialized()) { + m_LoadFailure = FailureType::INITIALIZATION; + } + else { + m_Runner->addDllSearchPath(pluginDataRoot / "dlls"); + } + + return true; +} + +QString ProxyPython::name() const +{ + return "Python Proxy"; +} + +QString ProxyPython::localizedName() const +{ + return tr("Python Proxy"); +} + +QString ProxyPython::author() const +{ + return "AnyOldName3, Holt59, Silarn, Tannin"; +} + +QString ProxyPython::description() const +{ + return tr("Proxy Plugin to allow plugins written in python to be loaded"); +} + +VersionInfo ProxyPython::version() const +{ + return VersionInfo(3, 0, 0, VersionInfo::RELEASE_FINAL); +} + +QList ProxyPython::settings() const +{ + return {}; +} + +QStringList ProxyPython::pluginList(const QDir& pluginPath) const +{ + QDir dir(pluginPath); + dir.setFilter(dir.filter() | QDir::NoDotAndDotDot); + QDirIterator iter(dir); + + // Note: We put python script (.py) and directory names, not the __init__.py + // files in those since it is easier for the runner to import them. + QStringList result; + while (iter.hasNext()) { + QString name = iter.next(); + QFileInfo info = iter.fileInfo(); + const QString baseName = info.fileName(); + + if (info.isFile() && name.endsWith(".py")) { + // Compatibility shims and disabled plugins staged on Linux; they are not + // loaded as MO2 plugins. + if (baseName == "winreg.py" || baseName == "lzokay.py" || + baseName == "FNISPatches.py" || baseName == "FNISTool.py" || + baseName == "FNISToolReset.py") { + continue; + } + result.append(name); + } + else if (info.isDir() && QDir(info.absoluteFilePath()).exists("__init__.py")) { + result.append(name); + } + } + + return result; +} + +QList ProxyPython::load(const QString& identifier) +{ + if (!m_Runner) { + return {}; + } + return m_Runner->load(identifier); +} + +void ProxyPython::unload(const QString& identifier) +{ + if (m_Runner) { + return m_Runner->unload(identifier); + } +} + +std::vector ProxyPython::activeProblems() const +{ + auto failure = m_LoadFailure; + + // don't know how this could happen but wth + if (m_Runner && !m_Runner->isInitialized()) { + failure = FailureType::INITIALIZATION; + } + + if (failure != FailureType::NONE) { + return {static_cast>(failure)}; + } + + return {}; +} + +QString ProxyPython::shortDescription(unsigned int key) const +{ + switch (static_cast(key)) { + case FailureType::SEMICOLON: + return tr("ModOrganizer path contains a semicolon"); + case FailureType::DLL_NOT_FOUND: + return tr("Python DLL not found"); + case FailureType::INVALID_DLL: + return tr("Invalid Python DLL"); + case FailureType::INITIALIZATION: + return tr("Initializing Python failed"); + default: + return tr("invalid problem key %1").arg(key); + } +} + +QString ProxyPython::fullDescription(unsigned int key) const +{ + switch (static_cast(key)) { + case FailureType::SEMICOLON: + return tr("The path to Mod Organizer (%1) contains a semicolon.
    " + "While this is legal on NTFS drives, many applications do not " + "handle it correctly.
    " + "Unfortunately MO depends on libraries that seem to fall into that " + "group.
    " + "As a result the python plugin cannot be loaded, and the only " + "solution we can offer is to remove the semicolon or move MO to a " + "path without a semicolon.") + .arg(QCoreApplication::applicationDirPath()); + case FailureType::DLL_NOT_FOUND: + return tr("The Python plugin DLL was not found, maybe your antivirus deleted " + "it. Re-installing MO2 might fix the problem."); + case FailureType::INVALID_DLL: + return tr( + "The Python plugin DLL is invalid, maybe your antivirus is blocking it. " + "Re-installing MO2 and adding exclusions for it to your AV might fix the " + "problem."); + case FailureType::INITIALIZATION: + return tr("The initialization of the Python plugin DLL failed, unfortunately " + "without any details."); + default: + return tr("invalid problem key %1").arg(key); + } +} + +bool ProxyPython::hasGuidedFix(unsigned int) const +{ + return false; +} + +void ProxyPython::startGuidedFix(unsigned int) const {} diff --git a/libs/plugin_python/src/proxy/proxypython.h b/libs/plugin_python/src/proxy/proxypython.h new file mode 100644 index 0000000..e824e2a --- /dev/null +++ b/libs/plugin_python/src/proxy/proxypython.h @@ -0,0 +1,80 @@ +/* +Copyright (C) 2022 Sebastian Herbord & MO2 Team. All rights reserved. + +This file is part of python proxy plugin for MO + +python proxy plugin is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Python proxy plugin is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with python proxy plugin. If not, see . +*/ + +#ifndef PROXYPYTHON_H +#define PROXYPYTHON_H + +#include +#include + +#include +#include + +#include + +class ProxyPython : public QObject, + public MOBase::IPluginProxy, + public MOBase::IPluginDiagnose { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginProxy MOBase::IPluginDiagnose) + Q_PLUGIN_METADATA(IID "org.mo2.ProxyPython") + +public: + ProxyPython(); + + virtual bool init(MOBase::IOrganizer* moInfo); + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + + QStringList pluginList(const QDir& pluginPath) const override; + QList load(const QString& identifier) override; + void unload(const QString& identifier) override; + +public: // IPluginDiagnose + virtual std::vector activeProblems() const override; + virtual QString shortDescription(unsigned int key) const override; + virtual QString fullDescription(unsigned int key) const override; + virtual bool hasGuidedFix(unsigned int key) const override; + virtual void startGuidedFix(unsigned int key) const override; + +private: + MOBase::IOrganizer* m_MOInfo; +#ifdef _WIN32 + HMODULE m_RunnerLib; +#else + void* m_RunnerLib; +#endif + std::unique_ptr m_Runner; + + enum class FailureType : unsigned int { + NONE = 0, + SEMICOLON = 1, + DLL_NOT_FOUND = 2, + INVALID_DLL = 3, + INITIALIZATION = 4 + }; + + FailureType m_LoadFailure; +}; + +#endif // PROXYPYTHON_H diff --git a/libs/plugin_python/src/pybind11-qt/CMakeLists.txt b/libs/plugin_python/src/pybind11-qt/CMakeLists.txt new file mode 100644 index 0000000..cb742ad --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/CMakeLists.txt @@ -0,0 +1,60 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Qt6 REQUIRED COMPONENTS Core Widgets) + +mo2_find_python_executable(PYTHON_EXE) + +add_library(pybind11-qt STATIC) +mo2_configure_target(pybind11-qt + NO_SOURCES + WARNINGS 4 + EXTERNAL_WARNINGS 4 + AUTOMOC OFF + TRANSLATIONS OFF +) +mo2_default_source_group() +target_sources(pybind11-qt + PRIVATE + ./include/pybind11_qt/pybind11_qt_basic.h + ./include/pybind11_qt/pybind11_qt_containers.h + ./include/pybind11_qt/pybind11_qt_enums.h + ./include/pybind11_qt/pybind11_qt_holder.h + ./include/pybind11_qt/pybind11_qt_objects.h + ./include/pybind11_qt/pybind11_qt_qflags.h + ./include/pybind11_qt/pybind11_qt.h + + pybind11_qt_basic.cpp + pybind11_qt_sip.cpp + pybind11_qt_utils.cpp + +) +mo2_target_sources(pybind11-qt + FOLDER src/details + PRIVATE + ./include/pybind11_qt/details/pybind11_qt_enum.h + ./include/pybind11_qt/details/pybind11_qt_qlist.h + ./include/pybind11_qt/details/pybind11_qt_qmap.h + ./include/pybind11_qt/details/pybind11_qt_sip.h + ./include/pybind11_qt/details/pybind11_qt_utils.h +) +target_link_libraries(pybind11-qt PUBLIC pybind11::pybind11 PRIVATE Qt6::Core Qt6::Widgets) +target_include_directories(pybind11-qt PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) + +# this is kind of broken but it only works with this... +target_compile_definitions(pybind11-qt PUBLIC QT_NO_KEYWORDS) + +# we need sip.h for pybind11-qt +add_custom_target(PyQt6-siph DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/sip.h") +set_target_properties(PyQt6-siph PROPERTIES FOLDER autogen) +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/sip.h" + COMMAND ${Python_EXECUTABLE} -m sipbuild.tools.module + --sip-h + --target-dir ${CMAKE_CURRENT_BINARY_DIR} + PyQt${MO2_QT_VERSION_MAJOR}.sip +) +add_dependencies(pybind11-qt PyQt6-siph) + +target_include_directories(pybind11-qt PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +add_library(pybind11::qt ALIAS pybind11-qt) diff --git a/libs/plugin_python/src/pybind11-qt/README.md b/libs/plugin_python/src/pybind11-qt/README.md new file mode 100644 index 0000000..784f9a8 --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/README.md @@ -0,0 +1,64 @@ +# pybind11-qt + +This library contains code to interface pybind11 with Qt and PyQt. + +## Type casters + +The main part of this library is a set of (templated) type casters for Qt types that +can be used by simply importing `pybind11_qt/pybind11_qt.h`. +This provides type casters for: + +- Standard Qt types, such as `QString` or `QVariant`. + - `QString` is equivalent to Python `str` (unicode or not). + - `QVariant` is not exposed but the object is directly converted, similarly to + `std::variant` default type caster. +- Qt containers (`QList`, `QSet`, `QMap`, `QStringList`). + - The `QList` type-caster is more flexible than the standard container type-casters + from pybind11 as it accepts any iterable. +- `QFlags` - Delegates the cast to the underlying type, basically. +- Qt enumerations: a lot of enumerations are provided in + [`pybind11_qt_enums.h`](include/pybind11_qt/pybind11_qt_enums.h) and new ones can be + easily added using the `PYQT_ENUM` macro (inside the header file). +- Qt objects: very few are provided in + [`pybind11_qt_objects.h`](include/pybind11_qt/pybind11_qt_objects.h) and new ones can + be added using the `PYQT_OBJECT` macro (inside the header file). + - Copy-constructible Qt objects are copied when passing from C++ to Python or + vice-versa. + - Non copy-constructible Qt objects, e.g., `QObject` or `QWidget` should always be + exposed as pointer, and their ownership is transferred to C++ when coming from + Python. + +## Qt holder + +The library also provides a `pybind11::qt::qobject_holder` holder for pybind11 that +transfer ownerships of the Python object to the underlying `QObject`. + +This holder is useful when exposing classes inheriting `QObject` (or a child class of +`QObject`) that can be extended to Python. + +The library also provides two `set_qt_owner` functions that can be used to transfer +ownership manually. + +## Qt delegates + +The library provides a `add_qt_delegate` function that can be used to delegate Python +call to Qt functions to C++: + +```cpp +py::class_< + // the C++ class extending QObject to expose + ISaveGameInfoWidget, + + // the trampoline class + PySaveGameInfoWidget, + + // the Qt holder to keep the Python object alive alongside the C++ one + py::qt::qobject_holder + +> iSaveGameInfoWidget(m, "ISaveGameInfoWidget"); + +// allow to access most of the class attributes through Python via an overload of +// __getattr__ and add a _widget() method to access the widget itself if needed +// +py::qt::add_qt_delegate(iSaveGameInfoWidget, "_widget"); +``` diff --git a/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_enum.h b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_enum.h new file mode 100644 index 0000000..45afd0c --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_enum.h @@ -0,0 +1,59 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_ENUM_HPP +#define PYTHON_PYBIND11_QT_DETAILS_ENUM_HPP + +#include + +#include + +#include "pybind11_qt_utils.h" + +namespace pybind11::detail::qt { + + // EnumData, with static members (const char[]) + // - package: name of the Python package containing the enum (e.g., + // PyQt6.QtCore) + // - name: full path to the enum, e.g. Qt.QGlobalColor + // + template + struct EnumData; + + // template class for most Qt types that have Python equivalent (QWidget, + // etc.) + // + template + struct qt_enum_caster { + + public: + PYBIND11_TYPE_CASTER(Enum, EnumData::package + const_name(".") + + EnumData::name); + + bool load(pybind11::handle src, bool) + { + if (PyLong_Check(src.ptr())) { + value = static_cast(PyLong_AsLong(src.ptr())); + return true; + } + + auto pyenum = + get_attr_rec(EnumData::package.text, EnumData::name.text); + + if (isinstance(src, pyenum)) { + value = static_cast(src.attr("value").cast()); + return true; + } + + return false; + } + + static pybind11::handle cast(Enum src, + pybind11::return_value_policy /* policy */, + pybind11::handle /* parent */) + { + auto pyenum = + get_attr_rec(EnumData::package.text, EnumData::name.text); + return pyenum(static_cast(src)); + } + }; +} // namespace pybind11::detail::qt + +#endif diff --git a/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qlist.h b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qlist.h new file mode 100644 index 0000000..e640725 --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qlist.h @@ -0,0 +1,52 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_QLIST_HPP +#define PYTHON_PYBIND11_QT_DETAILS_QLIST_HPP + +#include +#include + +namespace pybind11::detail::qt { + + // helper class for QList to construct from any proper iterable + // + template + struct qlist_caster { + using value_conv = make_caster; + + bool load(handle src, bool convert) + { + if (!isinstance(src) || isinstance(src) || + isinstance(src)) { + return false; + } + auto s = reinterpret_borrow(src); + value.clear(); + + if (isinstance(src)) { + value.reserve(s.cast().size()); + } + for (auto it : s) { + value_conv conv; + if (!conv.load(it, convert)) { + return false; + } + value.push_back(cast_op(std::move(conv))); + } + return true; + } + + template + static handle cast(T&& src, return_value_policy policy, handle parent) + { + return list_caster, Value>{}.cast(std::forward(src), policy, + parent); + } + + // we type these as "Sequence" even if these can be constructed from Iterable, + // otherwise the return type will be typed as "Iterable" which is problematic + PYBIND11_TYPE_CASTER(Type, const_name("Sequence[") + value_conv::name + + const_name("]")); + }; + +} // namespace pybind11::detail::qt + +#endif diff --git a/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qmap.h b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qmap.h new file mode 100644 index 0000000..ea743b7 --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qmap.h @@ -0,0 +1,70 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_QMAP_HPP +#define PYTHON_PYBIND11_QT_DETAILS_QMAP_HPP + +#include + +namespace pybind11::detail::qt { + + // helper class for QMap because QMap do not follow the standard std:: maps + // interface, for other containers, the pybind11 built-in xxx_caster works + // + // this code is basically a copy/paste from the pybind11 stl stuff with + // minor modifications + // + template + struct qmap_caster { + using key_conv = make_caster; + using value_conv = make_caster; + + bool load(handle src, bool convert) + { + if (!isinstance(src)) { + return false; + } + auto d = reinterpret_borrow(src); + value.clear(); + for (auto it : d) { + key_conv kconv; + value_conv vconv; + if (!kconv.load(it.first.ptr(), convert) || + !vconv.load(it.second.ptr(), convert)) { + return false; + } + value[cast_op(std::move(kconv))] = + cast_op(std::move(vconv)); + } + return true; + } + + template + static handle cast(T&& src, return_value_policy policy, handle parent) + { + dict d; + return_value_policy policy_key = policy; + return_value_policy policy_value = policy; + if (!std::is_lvalue_reference::value) { + policy_key = return_value_policy_override::policy(policy_key); + policy_value = + return_value_policy_override::policy(policy_value); + } + for (auto it = src.begin(); it != src.end(); ++it) { + auto key = reinterpret_steal( + key_conv::cast(forward_like(it.key()), policy_key, parent)); + auto value = reinterpret_steal(value_conv::cast( + forward_like(it.value()), policy_value, parent)); + if (!key || !value) { + return handle(); + } + d[key] = value; + } + return d.release(); + } + + PYBIND11_TYPE_CASTER(Type, const_name("Dict[") + key_conv::name + + const_name(", ") + value_conv::name + + const_name("]")); + }; + +} // namespace pybind11::detail::qt + +#endif diff --git a/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h new file mode 100644 index 0000000..556a980 --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h @@ -0,0 +1,221 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_SIP_HPP +#define PYTHON_PYBIND11_QT_DETAILS_SIP_HPP + +#include + +#include + +#include +#include + +#include "../pybind11_qt_holder.h" + +struct _sipTypeDef; +typedef struct _sipTypeDef sipTypeDef; + +struct _sipSimpleWrapper; +typedef struct _sipSimpleWrapper sipSimpleWrapper; + +struct _sipWrapper; +typedef struct _sipWrapper sipWrapper; + +namespace pybind11::detail::qt { + + // helper functions to avoid bringing in this header + namespace sip { + + // extract the underlying data if present from the equivalent PyQt object + void* extract_data(PyObject*); + + const sipTypeDef* api_find_type(const char* type); + int api_can_convert_to_type(PyObject* pyObj, const sipTypeDef* td, int flags); + + void api_transfer_to(PyObject* self, PyObject* owner); + void api_transfer_back(PyObject* self); + PyObject* api_convert_from_type(void* cpp, const sipTypeDef* td, + PyObject* transferObj); + } // namespace sip + + template + struct MetaData; + + template + struct MetaData>> + : MetaData> {}; + + // template class for most Qt types that have Python equivalent (QWidget, + // etc.) + // + template + struct qt_type_caster { + + static constexpr bool is_pointer = std::is_pointer_v; + using pointer = std::conditional_t; + + QClass value; + + public: + static constexpr auto name = MetaData::python_name; + + operator pointer() + { + if constexpr (is_pointer) { + return value; + } + else { + return &value; + } + } + + // pybind11 requires operator T&() & and operator T&&() && but here we want to + // use SFINAE with is_pointer so we need to template the operator + // + // having a template operator U&&() does not work since it will not + // deduce the proper return type for QClass&& or QClass& so we have two separate + // overloads, and in each one, U is actually a reference type (lvalue or rvalue) + // + + template , QClass> && + std::is_lvalue_reference_v && !is_pointer, + int> = 0> + operator U() + { + return value; + } + + template , QClass> && + std::is_rvalue_reference_v && !is_pointer, + int> = 0> + operator U() && + { + return std::move(value); + } + + template + using cast_op_type = + std::conditional_t>; + + bool load(pybind11::handle src, bool) + { + // special check for none for pointer classes + if constexpr (is_pointer) { + if (src.is_none()) { + value = nullptr; + return true; + } + } + + const auto* type = sip::api_find_type(MetaData::class_name); + if (type == nullptr) { + return false; + } + if (!sip::api_can_convert_to_type(src.ptr(), type, 0)) { + return false; + } + + // this would transfer responsibility for deconstructing the + // object to C++, but pybind11 assumes l-value converters (such + // as this) don't do that instead, this should be called within + // the wrappers for functions which return deletable pointers. + // + // sipAPI()->api_transfer_to(objPtr, Py_None); + // + void* const data = sip::extract_data(src.ptr()); + + if (data) { + if constexpr (is_pointer) { + value = reinterpret_cast(data); + + // transfer ownership + sip::api_transfer_to(src.ptr(), Py_None); + + // tie the py::object to the C++ one + new pybind11::detail::qt::qobject_holder_impl(value); + } + else { + value = *reinterpret_cast(data); + } + return true; + } + else { + return false; + } + } + + template < + typename T, + std::enable_if_t>::value, int> = 0> + static handle cast(T* src, return_value_policy policy, handle parent) + { + // note: when QClass is a pointer type, e.g. a QWidget*, T is a + // pointer to pointer, so we can defer to the standard cast() + + if (!src) { + return none().release(); + } + + if (!is_pointer && policy == return_value_policy::take_ownership) { + auto h = cast(std::move(*src), policy, parent); + delete src; + return h; + } + return cast(*src, policy, parent); + } + + static pybind11::handle cast(QClass src, pybind11::return_value_policy policy, + pybind11::handle /* parent */) + { + if constexpr (is_pointer) { + if (!src) { + return none().release(); + } + } + + const sipTypeDef* type = sip::api_find_type(MetaData::class_name); + if (type == nullptr) { + return Py_None; + } + + PyObject* sipObj; + void* sipData; + + if constexpr (is_pointer) { + sipData = src; + } + else if (std::is_copy_assignable_v) { + // we send to SIP a newly allocated object, and transfer the + // owernship to it + sipData = + new QClass(policy == ::pybind11::return_value_policy::take_ownership + ? std::move(src) + : src); + } + else { + sipData = &src; + } + + sipObj = sip::api_convert_from_type(sipData, type, 0); + + if (sipObj == nullptr) { + return Py_None; + } + + // ensure Python deletes the C++ component + if constexpr (!is_pointer) { + sip::api_transfer_back(sipObj); + } + else { + if (policy == return_value_policy::take_ownership) { + sip::api_transfer_back(sipObj); + } + } + + return sipObj; + } + }; + +} // namespace pybind11::detail::qt + +#endif diff --git a/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_utils.h b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_utils.h new file mode 100644 index 0000000..c877c15 --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_utils.h @@ -0,0 +1,47 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_UTILS_HPP +#define PYTHON_PYBIND11_QT_DETAILS_UTILS_HPP + +#include +#include + +#include + +namespace pybind11::detail::qt { + + /** + * @brief Convert a XXX::YYY compile time string to a XXX.YYY compile time + * string. Only one :: is allowed. + * + */ + template + constexpr descr qt_name_cpp2py(const char (&name)[N]) + { + descr res; + for (std::size_t i = 0, j = 0; i < N - 2; ++i) { + + res.text[i] = name[j]; + + if (res.text[i] == ':') { + res.text[i] = '.'; + j += 2; + } + else { + ++j; + } + } + return res; + } + + /** + * @brief Retrieve the class from the given package at the given path + * + * @param package Name of the module. + * @param path Path to the class/object in the module. + * + * @return the object at the given path in the given module + */ + pybind11::object get_attr_rec(std::string_view package, std::string_view path); + +} // namespace pybind11::detail::qt + +#endif diff --git a/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h new file mode 100644 index 0000000..e803e9f --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h @@ -0,0 +1,88 @@ +#ifndef PYTHON_PYBIND11_QT_HPP +#define PYTHON_PYBIND11_QT_HPP + +// this header defines many type casters for Qt types, including: +// - basic Qt types such as QString and QVariant - those do not have PyQt6 equivalent +// - QFlags<> class templates +// - containers such as QList<>, QSet<>, etc., the QList<> casters is more flexible than +// the std::vector<> or std::list<> ones as it accepts any iterable +// - many Qt enumeration types (see pybind11_qt_enums) +// - many Qt classes with PyQt6 equivalent +// - copyable type are copied between Python and C++ +// - non-copyable type (QObject, QWidget, QMainWindow) are always owned by the C++ +// side, even when constructed on the Python side, and owned their corresponding +// Python object, e.g., an instance of a class inheriting QWidget created on the +// Python side can be safely used in C++ since the Python object will be owned by +// the C++ QWidget object +// + +#include "pybind11_qt_basic.h" +#include "pybind11_qt_containers.h" +#include "pybind11_qt_enums.h" +#include "pybind11_qt_holder.h" +#include "pybind11_qt_objects.h" +#include "pybind11_qt_qflags.h" + +namespace pybind11::qt { + + /** + * @brief Tie the lifetime of the Python object to the lifetime of the given + * QObject. + * + * @param owner QObject that will own the python object. + * @param child Python object that the QObject will own. + */ + inline void set_qt_owner(QObject* owner, object child) + { + new detail::qt::qobject_holder_impl{owner, child}; + } + + /** + * @brief Tie the lifetime of the given object to the lifetime of the corresponding + * Python object. + * + * This object must have been created from Python and must inherit QObject. + * + * @param object Object to tie. + */ + template + void set_qt_owner(Class* object) + { + static_assert(std::is_base_of_v); + new detail::qt::qobject_holder_impl{object}; + } + + /** + * @brief Add Qt "delegate" to the given class. + * + * This function defines two methods: __getattr__ and name, where name will + * simply return the PyQtX object as a QClass* object, while __getattr__ + * will delegate to the underlying QClass object when required. + * + * This allow access to Qt interface for object exposed using pybind11 + * (e.g., signals, methods from QObject or QWidget, etc.). + * + * @param pyclass Python class to define the methods on. + * @param name Name of the method to retrieve the underlying object. + * + * @tparam QClass Name of the Qt class, cannot be deduced. + * @tparam Class Class being wrapped, deduced. + * @tparam Args... Arguments of the class template parameters, deduced. + */ + template + auto& add_qt_delegate(pybind11::class_& pyclass, const char* name) + { + return pyclass + .def(name, + [](Class* w) -> QClass* { + return w; + }) + .def( + "__getattr__", +[](Class* w, pybind11::str str) -> pybind11::object { + return pybind11::cast((QClass*)w).attr(str); + }); + } + +} // namespace pybind11::qt + +#endif diff --git a/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_basic.h b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_basic.h new file mode 100644 index 0000000..911de34 --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_basic.h @@ -0,0 +1,36 @@ +#ifndef PYTHON_PYBIND11_QT_BASIC_HPP +#define PYTHON_PYBIND11_QT_BASIC_HPP + +#include +#include + +#include + +namespace pybind11::detail { + + // QString + // + template <> + struct type_caster { + PYBIND11_TYPE_CASTER(QString, const_name("str")); + + bool load(handle src, bool); + + static handle cast(QString src, return_value_policy policy, handle parent); + }; + + // QVariant - this needs to be defined BEFORE QVariantList + // + template <> + struct type_caster { + public: + PYBIND11_TYPE_CASTER(QVariant, const_name("MoVariant")); + + bool load(handle src, bool); + + static handle cast(QVariant var, return_value_policy policy, handle parent); + }; + +} // namespace pybind11::detail + +#endif diff --git a/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_containers.h b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_containers.h new file mode 100644 index 0000000..120b5e8 --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_containers.h @@ -0,0 +1,51 @@ +#ifndef PYTHON_PYBIND11_QT_CONTAINERS_HPP +#define PYTHON_PYBIND11_QT_CONTAINERS_HPP + +#include +#include +#include + +#include +#include + +// this needs to be included here to get proper QVariantList and QVariantMap +#include "details/pybind11_qt_qlist.h" +#include "details/pybind11_qt_qmap.h" +#include "pybind11_qt_basic.h" + +namespace pybind11::detail { + + // QList + // + template + struct type_caster> : qt::qlist_caster, T> {}; + + // QSet + // + template + struct type_caster> : set_caster, T> {}; + + // QMap + // + template + struct type_caster> : qt::qmap_caster, K, V> {}; + + // QStringList + // + template <> + struct type_caster : qt::qlist_caster {}; + + // QVariantList + // + template <> + struct type_caster : qt::qlist_caster {}; + + // QVariantMap + // + template <> + struct type_caster : qt::qmap_caster { + }; + +} // namespace pybind11::detail + +#endif diff --git a/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_enums.h b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_enums.h new file mode 100644 index 0000000..b58cc41 --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_enums.h @@ -0,0 +1,115 @@ +#ifndef PYTHON_PYBIND11_QT_ENUMS_HPP +#define PYTHON_PYBIND11_QT_ENUMS_HPP + +#include +#include + +#include "details/pybind11_qt_enum.h" +#include "details/pybind11_qt_utils.h" + +#define PYQT_ENUM(QPackage, QEnum) \ + namespace pybind11::detail { \ + namespace qt { \ + template <> \ + struct EnumData { \ + constexpr static const auto package = \ + const_name("PyQt6.") + const_name(#QPackage); \ + constexpr static const auto name = qt_name_cpp2py(#QEnum); \ + }; \ + } \ + template <> \ + struct type_caster : qt::qt_enum_caster {}; \ + } + +PYQT_ENUM(QtCore, Qt::AlignmentFlag); +PYQT_ENUM(QtCore, Qt::AnchorPoint); +PYQT_ENUM(QtCore, Qt::ApplicationAttribute); +PYQT_ENUM(QtCore, Qt::ApplicationState); +PYQT_ENUM(QtCore, Qt::ArrowType); +PYQT_ENUM(QtCore, Qt::AspectRatioMode); +PYQT_ENUM(QtCore, Qt::Axis); +PYQT_ENUM(QtCore, Qt::BGMode); +PYQT_ENUM(QtCore, Qt::BrushStyle); +PYQT_ENUM(QtCore, Qt::CaseSensitivity); +PYQT_ENUM(QtCore, Qt::CheckState); +PYQT_ENUM(QtCore, Qt::ChecksumType); +PYQT_ENUM(QtCore, Qt::ClipOperation); +PYQT_ENUM(QtCore, Qt::ConnectionType); +PYQT_ENUM(QtCore, Qt::ContextMenuPolicy); +PYQT_ENUM(QtCore, Qt::CoordinateSystem); +PYQT_ENUM(QtCore, Qt::Corner); +PYQT_ENUM(QtCore, Qt::CursorMoveStyle); +PYQT_ENUM(QtCore, Qt::CursorShape); +PYQT_ENUM(QtCore, Qt::DateFormat); +PYQT_ENUM(QtCore, Qt::DayOfWeek); +PYQT_ENUM(QtCore, Qt::DockWidgetArea); +PYQT_ENUM(QtCore, Qt::DropAction); +PYQT_ENUM(QtCore, Qt::Edge); +PYQT_ENUM(QtCore, Qt::EnterKeyType); +PYQT_ENUM(QtCore, Qt::EventPriority); +PYQT_ENUM(QtCore, Qt::FillRule); +PYQT_ENUM(QtCore, Qt::FindChildOption); +PYQT_ENUM(QtCore, Qt::FocusPolicy); +PYQT_ENUM(QtCore, Qt::FocusReason); +PYQT_ENUM(QtCore, Qt::GestureFlag); +PYQT_ENUM(QtCore, Qt::GestureState); +PYQT_ENUM(QtCore, Qt::GestureType); +PYQT_ENUM(QtCore, Qt::GlobalColor); +PYQT_ENUM(QtCore, Qt::HitTestAccuracy); +PYQT_ENUM(QtCore, Qt::ImageConversionFlag); +PYQT_ENUM(QtCore, Qt::InputMethodHint); +PYQT_ENUM(QtCore, Qt::InputMethodQuery); +PYQT_ENUM(QtCore, Qt::ItemDataRole); +PYQT_ENUM(QtCore, Qt::ItemFlag); +PYQT_ENUM(QtCore, Qt::ItemSelectionMode); +PYQT_ENUM(QtCore, Qt::ItemSelectionOperation); +PYQT_ENUM(QtCore, Qt::Key); +PYQT_ENUM(QtCore, Qt::KeyboardModifier); +PYQT_ENUM(QtCore, Qt::LayoutDirection); +PYQT_ENUM(QtCore, Qt::MaskMode); +PYQT_ENUM(QtCore, Qt::MatchFlag); +PYQT_ENUM(QtCore, Qt::Modifier); +PYQT_ENUM(QtCore, Qt::MouseButton); +PYQT_ENUM(QtCore, Qt::MouseEventFlag); +PYQT_ENUM(QtCore, Qt::MouseEventSource); +PYQT_ENUM(QtCore, Qt::NativeGestureType); +PYQT_ENUM(QtCore, Qt::NavigationMode); +PYQT_ENUM(QtCore, Qt::Orientation); +PYQT_ENUM(QtCore, Qt::PenCapStyle); +PYQT_ENUM(QtCore, Qt::PenJoinStyle); +PYQT_ENUM(QtCore, Qt::PenStyle); +PYQT_ENUM(QtCore, Qt::ScreenOrientation); +PYQT_ENUM(QtCore, Qt::ScrollBarPolicy); +PYQT_ENUM(QtCore, Qt::ScrollPhase); +PYQT_ENUM(QtCore, Qt::ShortcutContext); +PYQT_ENUM(QtCore, Qt::SizeHint); +PYQT_ENUM(QtCore, Qt::SizeMode); +PYQT_ENUM(QtCore, Qt::SortOrder); +PYQT_ENUM(QtCore, Qt::TabFocusBehavior); +PYQT_ENUM(QtCore, Qt::TextElideMode); +PYQT_ENUM(QtCore, Qt::TextFlag); +PYQT_ENUM(QtCore, Qt::TextFormat); +PYQT_ENUM(QtCore, Qt::TextInteractionFlag); +PYQT_ENUM(QtCore, Qt::TileRule); +PYQT_ENUM(QtCore, Qt::TimeSpec); +PYQT_ENUM(QtCore, Qt::TimerType); +PYQT_ENUM(QtCore, Qt::ToolBarArea); +PYQT_ENUM(QtCore, Qt::ToolButtonStyle); +PYQT_ENUM(QtCore, Qt::TransformationMode); +PYQT_ENUM(QtCore, Qt::WhiteSpaceMode); +PYQT_ENUM(QtCore, Qt::WidgetAttribute); +PYQT_ENUM(QtCore, Qt::WindowFrameSection); +PYQT_ENUM(QtCore, Qt::WindowModality); +PYQT_ENUM(QtCore, Qt::WindowState); +PYQT_ENUM(QtCore, Qt::WindowType); + +PYQT_ENUM(QtWidgets, QMessageBox::ButtonRole); +PYQT_ENUM(QtWidgets, QMessageBox::DialogCode); +PYQT_ENUM(QtWidgets, QMessageBox::Icon); +PYQT_ENUM(QtWidgets, QMessageBox::PaintDeviceMetric); +PYQT_ENUM(QtWidgets, QMessageBox::RenderFlag); +PYQT_ENUM(QtWidgets, QMessageBox::StandardButton); + +#undef PYQT_ENUM + +#endif diff --git a/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_holder.h b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_holder.h new file mode 100644 index 0000000..39200d5 --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_holder.h @@ -0,0 +1,59 @@ +#ifndef PYTHON_PYBIND11_QT_HOLDER_HPP +#define PYTHON_PYBIND11_QT_HOLDER_HPP + +#include + +#include + +namespace pybind11::detail::qt { + + class qobject_holder_impl : public QObject { + object p_; + + public: + /** + * @brief Construct a new qobject holder linked to the given QObject and + * maintaining the given python object alive. + * + * @param p Parent of this holder. + * @param o Python object to keep alive. + */ + qobject_holder_impl(QObject* p, object o) : p_{o} { setParent(p); } + + template + qobject_holder_impl(U* p) + : qobject_holder_impl{p, reinterpret_borrow(cast(p))} + { + } + + ~qobject_holder_impl() + { + gil_scoped_acquire s; + p_ = object(); + } + }; + +} // namespace pybind11::detail::qt + +namespace pybind11::qt { + + template + class qobject_holder { + using type = Type; + + type* qobj_; + + public: + qobject_holder(type* qobj) : qobj_{qobj} + { + new detail::qt::qobject_holder_impl(qobj_); + } + + type* get() { return qobj_; } + }; + +} // namespace pybind11::qt + +PYBIND11_DECLARE_HOLDER_TYPE(T, ::pybind11::qt::qobject_holder) + +#endif diff --git a/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_objects.h b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_objects.h new file mode 100644 index 0000000..e16b5ea --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_objects.h @@ -0,0 +1,62 @@ +#ifndef PYTHON_PYBIND11_QT_OBJECTS_HPP +#define PYTHON_PYBIND11_QT_OBJECTS_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "details/pybind11_qt_sip.h" +#include "details/pybind11_qt_utils.h" + +#define PYQT_CLASS(QModule, QClass) \ + namespace pybind11::detail { \ + namespace qt { \ + template <> \ + struct MetaData { \ + constexpr static const auto class_name = #QClass; \ + constexpr static const auto python_name = \ + const_name("PyQt6.") + const_name(#QModule) + const_name(".") + \ + const_name(#QClass); \ + }; \ + } \ + template <> \ + struct type_caster \ + : std::conditional_t, \ + type_caster_generic, qt::qt_type_caster> {}; \ + template <> \ + struct type_caster \ + : std::conditional_t, \ + qt::qt_type_caster, type_caster> {}; \ + } + +// add declarations below to create bindings - the first argument is simply +// the name of the PyQt6 package containing the class, and is only used for +// the python signature + +PYQT_CLASS(QtCore, QByteArray); +PYQT_CLASS(QtCore, QDateTime); +PYQT_CLASS(QtCore, QDir); +PYQT_CLASS(QtCore, QFileInfo); +PYQT_CLASS(QtCore, QObject); +PYQT_CLASS(QtCore, QSize); +PYQT_CLASS(QtCore, QUrl); + +PYQT_CLASS(QtGui, QColor); +PYQT_CLASS(QtGui, QIcon); +PYQT_CLASS(QtGui, QPixmap); + +PYQT_CLASS(QtWidgets, QMainWindow); +PYQT_CLASS(QtWidgets, QWidget); + +#undef METADATA + +#endif diff --git a/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_qflags.h b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_qflags.h new file mode 100644 index 0000000..f246ed3 --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/include/pybind11_qt/pybind11_qt_qflags.h @@ -0,0 +1,56 @@ +#ifndef PYTHON_PYBIND11_QT_QFLAGS_HPP +#define PYTHON_PYBIND11_QT_QFLAGS_HPP + +#include + +#include + +namespace pybind11::detail { + + // QFlags + // + template + struct type_caster> { + PYBIND11_TYPE_CASTER(QFlags, const_name("QFlags[") + make_caster::name + + const_name("]")); + + /** + * Conversion part 1 (Python->C++): convert a PyObject into a QString + * instance or return false upon failure. The second argument + * indicates whether implicit conversions should be applied. + */ + bool load(handle src, bool) + { + PyObject* tmp = PyNumber_Long(src.ptr()); + + if (!tmp) { + return false; + } + + // we do an intermediate extraction to T but this actually + // can contains multiple values + T flag_value = static_cast(PyLong_AsLong(tmp)); + Py_DECREF(tmp); + + value = QFlags(flag_value); + + return !PyErr_Occurred(); + } + + /** + * Conversion part 2 (C++ -> Python): convert an QString instance into + * a Python object. The second and third arguments are used to + * indicate the return value policy and parent object (for + * ``return_value_policy::reference_internal``) and are generally + * ignored by implicit casters. + */ + static handle cast(QFlags const& src, return_value_policy /* policy */, + handle /* parent */) + { + return PyLong_FromLong(static_cast(src)); + } + }; + +} // namespace pybind11::detail + +#endif diff --git a/libs/plugin_python/src/pybind11-qt/pybind11_qt_basic.cpp b/libs/plugin_python/src/pybind11-qt/pybind11_qt_basic.cpp new file mode 100644 index 0000000..d82ec2b --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/pybind11_qt_basic.cpp @@ -0,0 +1,156 @@ +#include "pybind11_qt/pybind11_qt_basic.h" + +#include + +#include + +#include "pybind11_qt/details/pybind11_qt_utils.h" + +// need to import containers to get QVariantList and QVariantMap +#include "pybind11_qt/pybind11_qt_containers.h" + +namespace pybind11::detail { + + template + QString qstring_from_stdstring(std::basic_string const& s) + { + if constexpr (std::is_same_v) { + return QString::fromStdString(s); + } + else if constexpr (std::is_same_v) { + return QString::fromStdWString(s); + } + else if constexpr (std::is_same_v) { + return QString::fromStdU16String(s); + } + else if constexpr (std::is_same_v) { + return QString::fromStdU32String(s); + } + } + + /** + * Conversion part 1 (Python->C++): convert a PyObject into a QString + * instance or return false upon failure. The second argument + * indicates whether implicit conversions should be applied. + */ + bool type_caster::load(handle src, bool) + { + PyObject* objPtr = src.ptr(); + + if (PyBytes_Check(objPtr)) { + value = QString::fromUtf8(PyBytes_AsString(objPtr)); + return true; + } + else if (PyUnicode_Check(objPtr)) { + switch (PyUnicode_KIND(objPtr)) { + case PyUnicode_1BYTE_KIND: + value = QString::fromUtf8(PyUnicode_AsUTF8(objPtr)); + break; + case PyUnicode_2BYTE_KIND: + value = QString::fromUtf16( + reinterpret_cast(PyUnicode_2BYTE_DATA(objPtr)), + PyUnicode_GET_LENGTH(objPtr)); + break; + case PyUnicode_4BYTE_KIND: + value = QString::fromUcs4( + reinterpret_cast(PyUnicode_4BYTE_DATA(objPtr)), + PyUnicode_GET_LENGTH(objPtr)); + break; + default: + return false; + } + + return true; + } + else { + return false; + } + } + + /** + * Conversion part 2 (C++ -> Python): convert an QString instance into + * a Python object. The second and third arguments are used to + * indicate the return value policy and parent object (for + * ``return_value_policy::reference_internal``) and are generally + * ignored by implicit casters. + */ + handle type_caster::cast(QString src, return_value_policy /* policy */, + handle /* parent */) + { + return PyUnicode_DecodeUTF16(reinterpret_cast(src.utf16()), + 2 * src.length(), nullptr, 0); + } + + bool type_caster::load(handle src, bool) + { + // test for string first otherwise PyList_Check also works + if (PyBytes_Check(src.ptr()) || PyUnicode_Check(src.ptr())) { + value = src.cast(); + return true; + } + else if (PySequence_Check(src.ptr())) { + // we could check if all the elements can be converted to QString + // and store a QStringList in the QVariant but I am not sure that is + // really useful. + value = src.cast(); + return true; + } + else if (PyMapping_Check(src.ptr())) { + value = src.cast(); + return true; + } + else if (src.is(pybind11::none())) { + value = QVariant(); + return true; + } + else if (PyDict_Check(src.ptr())) { + value = src.cast(); + return true; + } + // PyBool will also return true for PyLong_Check but not the other way + // around, so the order here is relevant. + else if (PyBool_Check(src.ptr())) { + value = src.cast(); + return true; + } + else if (PyLong_Check(src.ptr())) { + // QVariant doesn't have long. It has int or long long. Given that + // on m/s, long is 32 bits for 32- and 64- bit code... + value = src.cast(); + return true; + } + else { + return false; + } + } + + handle type_caster::cast(QVariant var, return_value_policy policy, + handle parent) + { + switch (var.typeId()) { + case QMetaType::UnknownType: + return Py_None; + case QMetaType::Int: + return PyLong_FromLong(var.toInt()); + case QMetaType::UInt: + return PyLong_FromUnsignedLong(var.toUInt()); + case QMetaType::Bool: + return PyBool_FromLong(var.toBool()); + case QMetaType::QString: + return type_caster::cast(var.toString(), policy, parent); + // We need to check for StringList here because these are not considered + // List since List is QList will StringList is QList: + case QMetaType::QStringList: + return type_caster::cast(var.toStringList(), policy, parent); + case QMetaType::QVariantList: + return type_caster::cast(var.toList(), policy, parent); + case QMetaType::QVariantMap: + return type_caster::cast(var.toMap(), policy, parent); + default: { + PyErr_Format(PyExc_TypeError, "type unsupported: %d", var.userType()); + throw pybind11::error_already_set(); + } + } + } + +} // namespace pybind11::detail diff --git a/libs/plugin_python/src/pybind11-qt/pybind11_qt_sip.cpp b/libs/plugin_python/src/pybind11-qt/pybind11_qt_sip.cpp new file mode 100644 index 0000000..69ad4b8 --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/pybind11_qt_sip.cpp @@ -0,0 +1,105 @@ +#include "pybind11_qt/details/pybind11_qt_sip.h" + +#include + +#include + +#include + +namespace py = pybind11; + +namespace pybind11::detail::qt { + + const sipAPIDef* sipAPI() + { + std::string exception; + static const sipAPIDef* sipApi = nullptr; + if (sipApi == nullptr) { + PyImport_ImportModule("PyQt6.sip"); + + { + auto errorObj = PyErr_Occurred(); + if (errorObj != NULL) { + PyObject *type, *value, *traceback; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + if (traceback != NULL) { + py::handle h_type(type); + py::handle h_val(value); + py::handle h_tb(traceback); + py::object tb(py::module_::import("traceback")); + py::object fmt_exp(tb.attr("format_exception")); + py::object exp_list(fmt_exp(h_type, h_val, h_tb)); + py::object exp_str(py::str("\n").attr("join")(exp_list)); + exception = exp_str.cast(); + } + PyErr_Restore(type, value, traceback); + throw std::runtime_error{"Failed to load SIP API: " + exception}; + } + } + + sipApi = (const sipAPIDef*)PyCapsule_Import("PyQt6.sip._C_API", 0); + if (sipApi == NULL) { + auto errorObj = PyErr_Occurred(); + if (errorObj != NULL) { + PyObject *type, *value, *traceback; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + if (traceback != NULL) { + py::handle h_type(type); + py::handle h_val(value); + py::handle h_tb(traceback); + py::object tb(py::module_::import("traceback")); + py::object fmt_exp(tb.attr("format_exception")); + py::object exp_list(fmt_exp(h_type, h_val, h_tb)); + py::object exp_str(py::str("\n").attr("join")(exp_list)); + exception = exp_str.cast(); + } + PyErr_Restore(type, value, traceback); + } + throw std::runtime_error{"Failed to load SIP API: " + exception}; + } + } + return sipApi; + } + + namespace sip { + const sipTypeDef* api_find_type(const char* type) + { + return sipAPI()->api_find_type(type); + } + + int api_can_convert_to_type(PyObject* pyObj, const sipTypeDef* td, int flags) + { + return sipAPI()->api_can_convert_to_type(pyObj, td, flags); + } + + void api_transfer_to(PyObject* self, PyObject* owner) + { + sipAPI()->api_transfer_to(self, owner); + } + + void api_transfer_back(PyObject* self) + { + sipAPI()->api_transfer_back(self); + } + + PyObject* api_convert_from_type(void* cpp, const sipTypeDef* td, PyObject*) + { + return sipAPI()->api_convert_from_type(cpp, td, 0); + } + + void* extract_data(PyObject* ptr) + { + if (PyObject_TypeCheck(ptr, sipAPI()->api_simplewrapper_type)) { + return reinterpret_cast(ptr)->data; + } + else if (PyObject_TypeCheck(ptr, sipAPI()->api_wrapper_type)) { + return reinterpret_cast(ptr)->super.data; + } + return nullptr; + } + + } // namespace sip + +} // namespace pybind11::detail::qt diff --git a/libs/plugin_python/src/pybind11-qt/pybind11_qt_utils.cpp b/libs/plugin_python/src/pybind11-qt/pybind11_qt_utils.cpp new file mode 100644 index 0000000..249bed5 --- /dev/null +++ b/libs/plugin_python/src/pybind11-qt/pybind11_qt_utils.cpp @@ -0,0 +1,11 @@ +#include "pybind11_qt/details/pybind11_qt_utils.h" + +namespace pybind11::detail::qt { + + pybind11::object get_attr_rec(std::string_view package, std::string_view path) + { + + return module_::import("operator") + .attr("attrgetter")(path.data())(module_::import(package.data())); + } +} // namespace pybind11::detail::qt diff --git a/libs/plugin_python/src/pybind11-utils/CMakeLists.txt b/libs/plugin_python/src/pybind11-utils/CMakeLists.txt new file mode 100644 index 0000000..77895b6 --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(pybind11-utils STATIC + ./include/pybind11_utils/functional.h + ./include/pybind11_utils/generator.h + ./include/pybind11_utils/shared_cpp_owner.h + ./include/pybind11_utils/smart_variant_wrapper.h + ./include/pybind11_utils/smart_variant.h + + functional.cpp +) +mo2_configure_target(pybind11-utils + NO_SOURCES + WARNINGS 4 + EXTERNAL_WARNINGS 4 + AUTOMOC OFF + TRANSLATIONS OFF +) +mo2_default_source_group() +target_link_libraries(pybind11-utils PUBLIC pybind11::pybind11) +target_include_directories(pybind11-utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) + +add_library(pybind11::utils ALIAS pybind11-utils) diff --git a/libs/plugin_python/src/pybind11-utils/README.md b/libs/plugin_python/src/pybind11-utils/README.md new file mode 100644 index 0000000..46a9256 --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/README.md @@ -0,0 +1,50 @@ +# pybind11-utils + +This library contains some utility stuff for `pybind11` + +## smart_variant_wrapper.h + +Expose a function `mo2::python::wrap_arguments` and a template +`mo2::python::smart_variant` that can be used to expose more interesting types Python +than the C++ one, e.g., accept `os.PathLike` and `QFileInfo` when a simple `QString` is +expected. + +A toy example can be found in the test folder at +[`tests/python/test_argument_wrapper.cpp`](../../tests/python/test_argument_wrapper.cpp). + +More concrete examples can be found in +[`mobase/pybind11_all.h`](../mobase/pybind11_all.h) for `FileWrapper` and +`DirectoryWrapper`. + +## functional.h + +TODO: updated version of `` that should check the signature +a bit more when creating `std::function` (similar to previous implementation). + +## shared_cpp_owner.h + +Expose a macro `MO2_PYBIND11_SHARED_CPP_HOLDER` that can be used to declare that +`std::shared_ptr<...>` must hold-on their associate Python object. + +```cpp +// use the macro on the type to be exposed (with a trampoline class) +MO2_PYBIND11_SHARED_CPP_HOLDER(ISaveGame) + +// use std::shared_ptr<> as the holder for the class +py::class_>(...); +``` + +Using the `MO2_PYBIND11_SHARED_CPP_HOLDER` (must be present in all files manipulating +`ISaveGame` between C++ and Python) ensure that the Python instance remains alive +alongside the C++ one. + +The `MO2_PYBIND11_SHARED_CPP_HOLDER` declares a specialization of `type_caster<>` that +alters the `std::shared_ptr` by return a `std::shared_ptr<>` that owns the Python +object (via a `pybind11::object`) but does not release the C++ one - The C++ object is +owned by the Python one, so the relation is as follows: + +- The `std::shared_ptr` manipulated in C++ maintains the `pybind11::object` alive + through a custom deleter but DOES NOT release the C++ object when the reference count + reaches 0. +- The Python object holds a standard `std::shared_ptr` that will release the object + when the reference count reaches 0. diff --git a/libs/plugin_python/src/pybind11-utils/functional.cpp b/libs/plugin_python/src/pybind11-utils/functional.cpp new file mode 100644 index 0000000..4cc7b6c --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/functional.cpp @@ -0,0 +1,28 @@ +#include "pybind11_utils/functional.h" + +namespace py = pybind11; + +namespace mo2::python::detail { + + bool has_compatible_arity(py::function fn, std::size_t arity) + { + auto inspect = py::module_::import("inspect"); + auto arg_spec = inspect.attr("getfullargspec")(fn); + py::object args = arg_spec.attr("args"), varargs = arg_spec.attr("varargs"), + defaults = arg_spec.attr("defaults"); + + auto args_count = args.is(py::none()) ? 0 : py::len(args); + auto defaults_count = defaults.is(py::none()) ? 0 : py::len(defaults); + + if (inspect.attr("ismethod")(fn).cast() && py::hasattr(fn, "__self__")) { + --args_count; + } + + auto required_count = args_count - defaults_count; + + return required_count <= arity // cannot require more parameters than given, + && (args_count >= arity || + !varargs.is_none()); // must accept enough parameters. + } + +} // namespace mo2::python::detail diff --git a/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/functional.h b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/functional.h new file mode 100644 index 0000000..8662d82 --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/functional.h @@ -0,0 +1,155 @@ +#ifndef PYTHON_PYBIND11_FUNCTIONAL_H +#define PYTHON_PYBIND11_FUNCTIONAL_H + +#include + +namespace mo2::python::detail { + + // check if the given function is valid for a C++ function with the + // given arity + // + bool has_compatible_arity(pybind11::function handle, std::size_t arity); + +} // namespace mo2::python::detail + +namespace pybind11::detail { + + // custom type_caster for std::function<> + // + // most of this is from pybind11 except that we also check arity of the function to + // allow overloaded function based on arity of argument + // + template + struct type_caster> { + using type = std::function; + using retval_type = + conditional_t::value, void_type, Return>; + using function_type = Return (*)(Args...); + + public: + bool load(handle src, bool convert) + { + if (src.is_none()) { + // Defer accepting None to other overloads (if we aren't in convert + // mode): + if (!convert) { + return false; + } + return true; + } + + if (!isinstance(src)) { + return false; + } + + auto func = reinterpret_borrow(src); + + /* + When passing a C++ function as an argument to another C++ + function via Python, every function call would normally involve + a full C++ -> Python -> C++ roundtrip, which can be prohibitive. + Here, we try to at least detect the case where the function is + stateless (i.e. function pointer or lambda function without + captured variables), in which case the roundtrip can be avoided. + */ + if (auto cfunc = func.cpp_function()) { + auto* cfunc_self = PyCFunction_GET_SELF(cfunc.ptr()); + if (isinstance(cfunc_self)) { + auto c = reinterpret_borrow(cfunc_self); + auto* rec = (function_record*)c; + + while (rec != nullptr) { + if (rec->is_stateless && + same_type(typeid(function_type), + *reinterpret_cast( + rec->data[1]))) { + struct capture { + function_type f; + }; + value = ((capture*)&rec->data)->f; + return true; + } + rec = rec->next; + } + } + // PYPY segfaults here when passing builtin function like sum. + // Raising an fail exception here works to prevent the segfault, but + // only on gcc. See PR #1413 for full details + } + + // !MO2! - check arity + + if (!mo2::python::detail::has_compatible_arity(func, sizeof...(Args))) { + return false; + } + + // !MO2! - everything below is copy/paste from pybind11 + + // ensure GIL is held during functor destruction + struct func_handle { + function f; +#if !(defined(_MSC_VER) && _MSC_VER == 1916 && defined(PYBIND11_CPP17)) + // This triggers a syntax error under very special conditions (very + // weird indeed). + explicit +#endif + func_handle(function&& f_) noexcept + : f(std::move(f_)) + { + } + func_handle(const func_handle& f_) { operator=(f_); } + func_handle& operator=(const func_handle& f_) + { + gil_scoped_acquire acq; + f = f_.f; + return *this; + } + ~func_handle() + { + gil_scoped_acquire acq; + function kill_f(std::move(f)); + } + }; + + // to emulate 'move initialization capture' in C++11 + struct func_wrapper { + func_handle hfunc; + explicit func_wrapper(func_handle&& hf) noexcept : hfunc(std::move(hf)) + { + } + Return operator()(Args... args) const + { + gil_scoped_acquire acq; + object retval(hfunc.f(std::forward(args)...)); + return retval.template cast(); + } + }; + + value = func_wrapper(func_handle(std::move(func))); + return true; + } + + template + static handle cast(Func&& f_, return_value_policy policy, handle /* parent */) + { + if (!f_) { + return none().inc_ref(); + } + + auto result = f_.template target(); + if (result) { + return cpp_function(*result, policy).release(); + } + return cpp_function(std::forward(f_), policy).release(); + } + + PYBIND11_TYPE_CASTER(type, const_name("Callable[[") + + concat(make_caster::name...) + + const_name("], ") + + make_caster::name + + const_name("]")); + }; + +} // namespace pybind11::detail + +#endif diff --git a/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/generator.h b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/generator.h new file mode 100644 index 0000000..cbf9d18 --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/generator.h @@ -0,0 +1,57 @@ +#ifndef PYTHON_PYBIND11_GENERATOR_H +#define PYTHON_PYBIND11_GENERATOR_H + +#include + +#include + +namespace mo2::python { + + // the code here is mostly taken from pybind11 itself, and relies on some pybind11 + // internals so might be subject to change when upgrading pybind11 versions + + namespace detail { + template + struct generator_state { + std::generator g; + decltype(g.begin()) it; + + generator_state(std::generator gen) : g(std::move(gen)), it(g.begin()) {} + }; + } // namespace detail + + // create a Python generator from a C++ generator + // + template + auto make_generator(std::generator g, Args&&... args) + { + using state = detail::generator_state; + + namespace py = pybind11; + if (!py::detail::get_type_info(typeid(state), false)) { + py::class_(py::handle(), "iterator", pybind11::module_local()) + .def("__iter__", + [](state& s) -> state& { + return s; + }) + .def( + "__next__", + [](state& s) -> T { + if (s.it != s.g.end()) { + T v = *s.it; + s.it++; + return v; + } + else { + throw py::stop_iteration(); + } + }, + std::forward(args)...); + } + + return py::cast(state{std::move(g)}); + } + +} // namespace mo2::python + +#endif diff --git a/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/shared_cpp_owner.h b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/shared_cpp_owner.h new file mode 100644 index 0000000..02c2111 --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/shared_cpp_owner.h @@ -0,0 +1,90 @@ +#ifndef PYTHON_PYBIND11_SHARED_CPP_OWNER_H +#define PYTHON_PYBIND11_SHARED_CPP_OWNER_H + +#include + +// pybind11 has some issues when a Python classes extend a C++ wrapper since the Python +// object is not kept alive alongside the returned object +// +// there is a pybind11 branch called "smart_holder" that tries to solve this in a very +// complicated way (with many other features) +// +// here, we simply use a custom type_caster<> for the classes we need - see the actual +// definition in mo2::python::detail below +// +// IMPORTANT: this only works for classes that are managed by shared_ptr on the C++ +// side, not Qt object (see pybind11-qt holder for that) +// + +namespace mo2::python::detail { + + template + struct shared_cpp_owner_caster + : pybind11::detail::copyable_holder_caster { + + // note that the actual holder type might be different in term of constness + using type = Type; + using holder_type = SharedType; + + using base = pybind11::detail::copyable_holder_caster; + using base::holder; + using base::value; + + // in load, we use the default type_caster<> to extract the shared pointer, then + // we replace it by a custom one + // + // the custom shared_ptr<> holds the py::object BUT does not really manage the + // C++ object because it will ref-count but not delete it + // + // this should work because here it's how it works: + // - the Python object holds a standard shared_ptr<> for the C++ object -> the + // C++ object remains alive as long as the Python one remains alive + // - the C++ object holds a shared_ptr<> that manages the python object -> the + // Python object remains alive as-long as there is a shared_ptr<> on the C++ + // side + // + bool load(pybind11::handle src, bool convert) + { + namespace py = pybind11; + + if (!base::load(src, convert)) { + return false; + } + + holder.reset(holder.get(), [pyobj = py::reinterpret_borrow( + src)](auto*) mutable { + py::gil_scoped_acquire s; + pyobj = py::object(); + + // we do NOT delete the object here - if this was the last reference to + // the Python object, the Python object will delete it + }); + + return true; + } + + // cast simply forward to the original type_caster<> + // + static pybind11::handle cast(const holder_type& src, + pybind11::return_value_policy policy, + pybind11::handle parent) + { + return base::cast(src, policy, parent); + } + }; + +} // namespace mo2::python::detail + +#define MO2_PYBIND11_SHARED_CPP_HOLDER(Type) \ + namespace pybind11::detail { \ + template <> \ + struct type_caster> \ + : mo2::python::detail::shared_cpp_owner_caster> {}; \ + template <> \ + struct type_caster> \ + : mo2::python::detail::shared_cpp_owner_caster< \ + Type, std::shared_ptr> {}; \ + } + +#endif diff --git a/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant.h b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant.h new file mode 100644 index 0000000..bd7bd9b --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant.h @@ -0,0 +1,53 @@ +#ifndef PYTHON_PYBIND11_UTILS_SMART_VARIANT_H +#define PYTHON_PYBIND11_UTILS_SMART_VARIANT_H + +#include + +namespace mo2::python { + + namespace detail { + + // simple template class that should be specialized to expose proper fromXXX + // methods + // + template + struct smart_variant_converter { + template + static T from(U&& u) + { + return T{std::forward(u)}; + } + }; + + } // namespace detail + + // a smart_variant is a std::variant that can be automatically converted to any of + // its type via custom operator T() + // + // user should specialize detail::smart_variant_converter to provide proper + // conversions + // + template + struct smart_variant : std::variant { + using std::variant::variant; + + template ...>, int> = 0> + operator T() const + { + return std::visit( + [](auto const& t) -> T { + if constexpr (std::is_same_v, T>) { + return t; + } + else { + return detail::smart_variant_converter::from(t); + } + }, + *this); + } + }; + +} // namespace mo2::python + +#endif diff --git a/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant_wrapper.h b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant_wrapper.h new file mode 100644 index 0000000..123d95c --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant_wrapper.h @@ -0,0 +1,180 @@ +#ifndef PYTHON_PYBIND11_UTILS_SMART_VARIANT_WRAPPER_H +#define PYTHON_PYBIND11_UTILS_SMART_VARIANT_WRAPPER_H + +#include +#include + +#include +#include + +#include "smart_variant.h" + +namespace mo2::python { + + namespace detail { + + // simple helper class that expose a ::type attribute which is U is I is in Is, + // V otherwise + template + struct wrap_arg; + + template + struct wrap_arg> { + using type = + std::conditional_t...>, + U, V>; + }; + + // helper type for wrap_arg + template + using wrap_arg_t = typename wrap_arg::type; + + template + auto wrap_arguments_impl(std::index_sequence, Fn&& fn, R (*)(Args...), + std::index_sequence) + { + return [fn = std::forward(fn)]( + wrap_arg_t>... args) { + return std::invoke(fn, std::forward(args)...); + }; + } + + template + auto make_convertible_index_sequence(std::index_sequence) + { + return std::index_sequence<(std::is_convertible_v ? Is : -1)...>{}; + } + + template + auto wrap_arguments_impl(Fn&& fn, R (*sg)(Args...)) + { + if constexpr (sizeof...(Is) == 0) { + return wrap_arguments_impl( + make_convertible_index_sequence( + std::make_index_sequence{}), + std::forward(fn), sg, + std::make_index_sequence{}); + } + else { + return wrap_arguments_impl( + std::index_sequence{}, std::forward(fn), sg, + std::make_index_sequence{}); + } + } + + template + auto wrap_return_impl(Fn&& fn, R (*)(Args...)) + { + return [fn = std::forward(fn)](Args... args) { + return T{std::invoke(fn, std::forward(args)...)}; + }; + } + + // make_python_function_signature: return a null-pointer with the proper type + // for the given function + + template + struct function_signature { + using type = + pybind11::detail::function_signature_t>; + }; + + template + struct function_signature { + using type = R(Args...); + }; + + template + struct function_signature { + using type = R(C*, Args...); + }; + + template + struct function_signature { + using type = R(C*, Args...); + }; + + template + struct function_signature { + using type = R(const C*, Args...); + }; + + template + struct function_signature { + using type = R(const C*, Args...); + }; + + template + using function_signature_t = typename function_signature::type; + + template + class wrap_type_caster { + using variant_type = std::variant; + using variant_caster = pybind11::detail::make_caster; + + public: + PYBIND11_TYPE_CASTER(Type, variant_caster::name); + + bool load(pybind11::handle src, bool convert) + { + variant_caster caster; + + if (!caster.load(src, convert)) { + return false; + } + + value = std::visit( + [](auto const& fn) { + return Type(fn); + }, + static_cast(caster)); + return true; + } + + static pybind11::handle cast(const Type& src, + pybind11::return_value_policy policy, + pybind11::handle parent) + { + return variant_caster::cast(variant_type(std::in_place_index<0>, src), + policy, parent); + } + }; + + } // namespace detail + + // wrap the given function-like object to accept T instead of the specified + // arguments at the specified positions + // + // if the list of positions is empty, replace all arguments that can be converted to + // T + // + template + auto wrap_arguments(Fn&& fn) + { + return detail::wrap_arguments_impl( + std::forward(fn), + (mo2::python::detail::function_signature_t*)nullptr); + } + + // wrap the given function-like object to return T instead of the specified type + // + template + auto wrap_return(Fn&& fn) + { + return detail::wrap_return_impl( + std::forward(fn), + (mo2::python::detail::function_signature_t*)nullptr); + } + +} // namespace mo2::python + +namespace pybind11::detail { + + template + struct type_caster<::mo2::python::smart_variant> + : variant_caster<::mo2::python::smart_variant> {}; + +} // namespace pybind11::detail + +#endif diff --git a/libs/plugin_python/src/runner/CMakeLists.txt b/libs/plugin_python/src/runner/CMakeLists.txt new file mode 100644 index 0000000..d164460 --- /dev/null +++ b/libs/plugin_python/src/runner/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.16) + +if(NOT TARGET mo2::uibase) + find_package(mo2-uibase CONFIG REQUIRED) +endif() + +add_library(runner SHARED + error.h + pythonrunner.cpp + pythonrunner.h + pythonutils.h + pythonutils.cpp +) +mo2_configure_target(runner + NO_SOURCES + WARNINGS 4 + EXTERNAL_WARNINGS 4 + AUTOMOC ON + TRANSLATIONS OFF +) +mo2_default_source_group() +target_link_libraries(runner PUBLIC mo2::uibase PRIVATE pybind11::embed pybind11::qt) +target_include_directories(runner PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(runner PRIVATE RUNNER_BUILD) +if(NOT WIN32 AND Python_SHARED_LIBRARY) + get_filename_component(_pylib_name "${Python_SHARED_LIBRARY}" NAME) + target_compile_definitions(runner PRIVATE + MO2_PYTHON_SHARED_LIBRARY="${_pylib_name}") +endif() + +# proxy will install runner + +# force runner to build mobase +add_dependencies(runner mobase) diff --git a/libs/plugin_python/src/runner/error.h b/libs/plugin_python/src/runner/error.h new file mode 100644 index 0000000..341c7ea --- /dev/null +++ b/libs/plugin_python/src/runner/error.h @@ -0,0 +1,82 @@ +#ifndef ERROR_H +#define ERROR_H + +#include + +#include + +#include + +#include + +namespace pyexcept { + + /** + * @brief Exception to throw when a python implementation does not implement + * a pure virtual function. + */ + class MissingImplementation : public MOBase::Exception { + public: + MissingImplementation(std::string const& className, + std::string const& methodName) + : Exception(QString::fromStdString( + std::format("Python class implementing \"{}\" has no " + "implementation of method \"{}\".", + className, methodName))) + { + } + }; + + /** + * @brief Exception to throw when a python error occurs. + */ + class PythonError : public MOBase::Exception { + public: + /** + * @brief Create a new PythonError, fetching the error message from + * python. If the message cannot be retrieved, `defaultErrorMessage()` + * is used instead. + */ + PythonError(pybind11::error_already_set const& ex) : Exception(ex.what()) {} + + /** + * @brief Create a new PythonError with the given message. + * + * @param message Message for the exception. + */ + PythonError(QString message) : Exception(message) {} + }; + + /** + * @brief Exception to throw when an unknown error occured. This is + * typically thrown from a catch(...) block. + */ + class UnknownException : public MOBase::Exception { + public: + /** + * @brief Create a new UnknownException with the default message. + * + * @see defaultErrorMessage + */ + UnknownException() : Exception(defaultErrorMessage()) {} + + /** + * @brief Create a new UnknownException with the given message. + * + * @param message Message for the exception. + */ + UnknownException(QString message) : Exception(message) {} + + protected: + /** + * + */ + static QString defaultErrorMessage() + { + return QObject::tr("An unknown exception was thrown in python code."); + } + }; + +} // namespace pyexcept + +#endif // ERROR_H diff --git a/libs/plugin_python/src/runner/pythonrunner.cpp b/libs/plugin_python/src/runner/pythonrunner.cpp new file mode 100644 index 0000000..05a0f30 --- /dev/null +++ b/libs/plugin_python/src/runner/pythonrunner.cpp @@ -0,0 +1,415 @@ +#include "pythonrunner.h" + +#ifdef _WIN32 +#pragma warning(disable : 4100) +#pragma warning(disable : 4996) + +#include +#else +#include +#endif + +#include +#include +#include + +#include +#include +#include + +#include "pybind11_qt/pybind11_qt.h" +#include +#include +#include + +#include +#include + +#include "error.h" +#include "pythonutils.h" + +using namespace MOBase; +namespace py = pybind11; + +namespace mo2::python { + + /** + * + */ + class PythonRunner : public IPythonRunner { + + public: + PythonRunner() = default; + ~PythonRunner() = default; + + QList load(const QString& identifier) override; + void unload(const QString& identifier) override; + + bool initialize(std::vector const& pythonPaths) override; + void addDllSearchPath(std::filesystem::path const& dllPath) override; + bool isInitialized() const override; + + private: + /** + * @brief Ensure that the given folder is in sys.path. + */ + void ensureFolderInPath(QString folder); + + private: + // for each "identifier" (python file or python module folder), contains the + // list of python objects - this does not keep the objects alive, it simply used + // to unload plugins + std::unordered_map> m_PythonObjects; + }; + + std::unique_ptr createPythonRunner() + { + return std::make_unique(); + } + + bool PythonRunner::initialize(std::vector const& pythonPaths) + { + // we only initialize Python once for the whole lifetime of the program, even if + // MO2 is restarted and the proxy or PythonRunner objects are deleted and + // recreated, Python is not re-initialized + // + // in an ideal world, we would initialize Python here (or in the constructor) + // and then finalize it in the destructor + // + // unfortunately, many library, including PyQt6, do not handle properly + // re-initializing the Python interpreter, so we cannot do that and we keep the + // interpreter alive + // + + if (Py_IsInitialized()) { + return true; + } + + std::optional oldPythonHome; + std::optional oldPythonPath; + auto restorePythonEnv = [&]() { + if (oldPythonHome.has_value()) { + setenv("PYTHONHOME", oldPythonHome->constData(), 1); + } else { + unsetenv("PYTHONHOME"); + } + if (oldPythonPath.has_value()) { + setenv("PYTHONPATH", oldPythonPath->constData(), 1); + } else { + unsetenv("PYTHONPATH"); + } + }; + + try { + static const char* argv0 = "ModOrganizer.exe"; + +#ifndef _WIN32 +#ifdef MO2_PYTHON_SHARED_LIBRARY + // Ensure libpython symbols are globally visible for extension modules + // loaded later (_struct, PyQt6, etc.). + void* pyHandle = + dlopen(MO2_PYTHON_SHARED_LIBRARY, RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD); + if (pyHandle == nullptr) { + pyHandle = dlopen(MO2_PYTHON_SHARED_LIBRARY, RTLD_NOW | RTLD_GLOBAL); + } + if (pyHandle == nullptr) { + MOBase::log::warn("failed to dlopen python shared library '{}': {}", + MO2_PYTHON_SHARED_LIBRARY, dlerror()); + } +#endif +#endif + + // For portable/AppImage builds, set PYTHONHOME so the interpreter + // finds the bundled stdlib instead of looking at system paths. + // MO2_PYTHON_DIR (set by AppRun) points to the writable python/ + // dir next to the AppImage; fall back to /python. + QString pythonHome; + const char* envPy = std::getenv("MO2_PYTHON_DIR"); + if (envPy && envPy[0] != '\0') { + pythonHome = QString::fromUtf8(envPy); + } else { + pythonHome = QCoreApplication::applicationDirPath() + "/python"; + } + if (const char* v = std::getenv("PYTHONHOME"); v != nullptr) { + oldPythonHome = QByteArray(v); + } + if (const char* v = std::getenv("PYTHONPATH"); v != nullptr) { + oldPythonPath = QByteArray(v); + } + + if (QDir(pythonHome).exists()) { + setenv("PYTHONHOME", pythonHome.toUtf8().constData(), 1); + + const QDir libDir(pythonHome + "/lib"); + const auto pyDirs = + libDir.entryList({"python3.*"}, QDir::Dirs | QDir::NoDotAndDotDot); + if (!pyDirs.isEmpty()) { + const QString pyver = pyDirs.first(); + const QString pyPath = QString("%1/lib/%2:%1/lib/%2/site-packages:%1") + .arg(pythonHome, pyver); + setenv("PYTHONPATH", pyPath.toUtf8().constData(), 1); + } + } + + // Paths we want to prepend/append for MO2 plugin loading. + auto paths = pythonPaths; + + PyConfig config; + PyConfig_InitPythonConfig(&config); + + // from PyBind11 + config.parse_argv = 0; + config.install_signal_handlers = 0; + + // from MO2 + config.site_import = 1; + config.optimization_level = 2; + + py::initialize_interpreter(&config, 1, &argv0, true); + + // Restore process environment after interpreter startup so + // subprocesses (umu/NaK/tools) are not forced onto MO2's Python. + restorePythonEnv(); + + if (!Py_IsInitialized()) { + MOBase::log::error( + "failed to init python: failed to initialize interpreter."); + + if (PyGILState_Check()) { + PyEval_SaveThread(); + } + + return false; + } + + { + for (auto const& path : paths) { + ensureFolderInPath(QString::fromStdString(absolute(path).string())); + } + + py::module_ mainModule = py::module_::import("__main__"); + py::object mainNamespace = mainModule.attr("__dict__"); + mainNamespace["sys"] = py::module_::import("sys"); + mainNamespace["mobase"] = py::module_::import("mobase"); + + mo2::python::configure_python_stream(); + mo2::python::configure_python_logging(mainNamespace["mobase"]); + } + + // we need to release the GIL here - which is what this does + // + // when Python is initialized, the GIl is acquired, and if it is not + // release, trying to acquire it on a different thread will deadlock + PyEval_SaveThread(); + + return true; + } + catch (const py::error_already_set& ex) { + restorePythonEnv(); + MOBase::log::error("failed to init python: {}", ex.what()); + return false; + } + } + + void PythonRunner::addDllSearchPath(std::filesystem::path const& dllPath) + { + py::gil_scoped_acquire lock; +#ifdef _WIN32 + py::module_::import("os").attr("add_dll_directory")(absolute(dllPath)); +#else + // On Linux, there is no add_dll_directory equivalent; prepend the folder to + // sys.path so Python extension modules can be found. + ensureFolderInPath(QString::fromStdString(absolute(dllPath).string())); +#endif + } + + void PythonRunner::ensureFolderInPath(QString folder) + { + py::module_ sys = py::module_::import("sys"); + py::list sysPath = sys.attr("path"); + + // Converting to QStringList for Qt::CaseInsensitive and because .index() + // raise an exception: + const QStringList currentPath = sysPath.cast(); + if (!currentPath.contains(folder, Qt::CaseInsensitive)) { + sysPath.insert(0, folder); + } + } + + QList PythonRunner::load(const QString& identifier) + { + py::gil_scoped_acquire lock; + + const QFileInfo idInfo(identifier); + const QString baseName = idInfo.fileName(); + if (baseName == "winreg.py" || baseName == "lzokay.py") { + log::debug("Skipping Python compatibility shim '{}'.", identifier); + return {}; + } + + // `pluginName` can either be a python file (single-file plugin or a folder + // (whole module). + // + // For whole module, we simply add the parent folder to path, then we load + // the module with a simple py::import, and we retrieve the associated + // __dict__ from which we extract either createPlugin or createPlugins. + // + // For single file, we need to use py::eval_file, and we will use the + // context (global variables) from __main__ (already contains mobase, and + // other required module). Since the context is shared between called of + // `instantiate`, we need to make sure to remove createPlugin(s) from + // previous call. + try { + + // dictionary that will contain createPlugin() or createPlugins(). + py::dict moduleDict; + + if (identifier.endsWith(".py")) { + py::object mainModule = py::module_::import("__main__"); + + // make a copy, otherwise we might end up calling the createPlugin() or + // createPlugins() function multiple time + py::dict moduleNamespace = mainModule.attr("__dict__").attr("copy")(); + + std::string temp = ToString(identifier); + py::eval_file(temp, moduleNamespace).is_none(); + moduleDict = moduleNamespace; + } + else { + // Retrieve the module name: + QStringList parts = identifier.split("/"); + std::string moduleName = ToString(parts.takeLast()); + ensureFolderInPath(parts.join("/")); + + // check if the module is already loaded + py::dict modules = py::module_::import("sys").attr("modules"); + if (modules.contains(moduleName)) { + py::module_ prev = modules[py::str(moduleName)]; + py::module_(prev).reload(); + moduleDict = prev.attr("__dict__"); + } + else { + moduleDict = + py::module_::import(moduleName.c_str()).attr("__dict__"); + } + } + + if (py::len(moduleDict) == 0) { + MOBase::log::error("No plugins found in {}.", identifier); + return {}; + } + + // Create the plugins: + std::vector plugins; + + if (moduleDict.contains("createPlugin")) { + plugins.push_back(moduleDict["createPlugin"]()); + } + else if (moduleDict.contains("createPlugins")) { + py::object pyPlugins = moduleDict["createPlugins"](); + if (!py::isinstance(pyPlugins)) { + MOBase::log::error( + "Plugin {}: createPlugins must return a sequence.", identifier); + } + else { + py::sequence pyList(pyPlugins); + size_t nPlugins = pyList.size(); + for (size_t i = 0; i < nPlugins; ++i) { + plugins.push_back(pyList[i]); + } + } + } + else { + MOBase::log::error("Plugin {}: missing a createPlugin(s) function.", + identifier); + } + + // If we have no plugins, there was an issue, and we already logged the + // problem: + if (plugins.empty()) { + return QList(); + } + + QList allInterfaceList; + + for (py::object pluginObj : plugins) { + + // save to be able to unload it + m_PythonObjects[identifier].push_back(pluginObj); + + QList interfaceList = py::module_::import("mobase.private") + .attr("extract_plugins")(pluginObj) + .cast>(); + + if (interfaceList.isEmpty()) { + MOBase::log::error("Plugin {}: no plugin interface implemented.", + identifier); + } + + // Append the plugins to the main list: + allInterfaceList.append(interfaceList); + } + + return allInterfaceList; + } + catch (const py::error_already_set& ex) { + MOBase::log::error("Failed to import plugin from {}.", identifier); + throw pyexcept::PythonError(ex); + } + } + + void PythonRunner::unload(const QString& identifier) + { + auto it = m_PythonObjects.find(identifier); + if (it != m_PythonObjects.end()) { + + py::gil_scoped_acquire lock; + + if (!identifier.endsWith(".py")) { + + // At this point, the identifier is the full path to the module. + QDir folder(identifier); + + // We want to "unload" (remove from sys.modules) modules that come + // from this plugin (whose __path__ points under this module, + // including the module of the plugin itself). + py::object sys = py::module_::import("sys"); + py::dict modules = sys.attr("modules"); + py::list keys = modules.attr("keys")(); + for (std::size_t i = 0; i < py::len(keys); ++i) { + py::object mod = modules[keys[i]]; + if (PyObject_HasAttrString(mod.ptr(), "__path__")) { + QString mpath = + mod.attr("__path__")[py::int_(0)].cast(); + + if (!folder.relativeFilePath(mpath).startsWith("..")) { + // If the path is under identifier, we need to unload + // it. + log::debug("Unloading module {} from {} for {}.", + keys[i].cast(), mpath, identifier); + + PyDict_DelItem(modules.ptr(), keys[i].ptr()); + } + } + } + } + + // Boost.Python does not handle cyclic garbace collection, so we need to + // release everything hold by the objects before deleting the objects + // themselves (done when erasing from m_PythonObjects). + for (auto& obj : it->second) { + obj.attr("__dict__").attr("clear")(); + } + + log::debug("Deleting {} python objects for {}.", it->second.size(), + identifier); + m_PythonObjects.erase(it); + } + } + + bool PythonRunner::isInitialized() const + { + return Py_IsInitialized() != 0; + } + +} // namespace mo2::python diff --git a/libs/plugin_python/src/runner/pythonrunner.h b/libs/plugin_python/src/runner/pythonrunner.h new file mode 100644 index 0000000..5f9751b --- /dev/null +++ b/libs/plugin_python/src/runner/pythonrunner.h @@ -0,0 +1,53 @@ +#ifndef PYTHONRUNNER_H +#define PYTHONRUNNER_H + +#include +#include + +#include +#include +#include +#include + +#ifdef RUNNER_BUILD +#define RUNNER_DLL_EXPORT Q_DECL_EXPORT +#else +#define RUNNER_DLL_EXPORT Q_DECL_IMPORT +#endif + +namespace mo2::python { + + // python runner interface + // + class IPythonRunner { + public: + virtual QList load(const QString& identifier) = 0; + virtual void unload(const QString& identifier) = 0; + + // initialize Python + // + // pythonPaths contains the list of built-in paths for the Python library + // (pythonxxx.zip, etc.), an empty list uses the default Python paths (e.g., the + // PYTHONPATH environment variable) + // + virtual bool + initialize(std::vector const& pythonPaths = {}) = 0; + + // add a DLL search path + // + virtual void addDllSearchPath(std::filesystem::path const& dllPath) = 0; + + // check if the runner has been initialized, i.e., initialize() has been + // called and succeeded + virtual bool isInitialized() const = 0; + + virtual ~IPythonRunner() {} + }; + + // create the Python runner + // + RUNNER_DLL_EXPORT std::unique_ptr createPythonRunner(); + +} // namespace mo2::python + +#endif // PYTHONRUNNER_H diff --git a/libs/plugin_python/src/runner/pythonutils.cpp b/libs/plugin_python/src/runner/pythonutils.cpp new file mode 100644 index 0000000..c94a50b --- /dev/null +++ b/libs/plugin_python/src/runner/pythonutils.cpp @@ -0,0 +1,155 @@ +#include "pythonutils.h" + +#include +#include +#include + +#include +#include + +#include + +namespace py = pybind11; + +namespace mo2::python { + + class PrintWrapper { + MOBase::log::Levels level_; + std::stringstream buffer_; + + public: + PrintWrapper(MOBase::log::Levels level) : level_{level} {} + + void write(std::string_view message) + { + buffer_ << message; + if (buffer_.tellp() != 0 && buffer_.str().back() == '\n') { + const auto full_message = buffer_.str(); + MOBase::log::log(level_, "{}", + full_message.substr(0, full_message.length() - 1)); + buffer_ = std::stringstream{}; + } + } + }; + + /** + * @brief Construct a dynamic Python type. + * + */ + template + pybind11::object make_python_type(std::string_view name, + pybind11::tuple base_classes, Args&&... args) + { + // this is ugly but that's how it's done in C Python + auto type = py::reinterpret_borrow((PyObject*)&PyType_Type); + + // create the python class + return type(name, base_classes, py::dict(std::forward(args)...)); + } + + void configure_python_stream() + { + // create the "MO2Handler" python class + auto printWrapper = make_python_type( + "MO2PrintWrapper", py::make_tuple(), + py::arg("write") = py::cpp_function([](std::string_view message) { + static PrintWrapper wrapper(MOBase::log::Debug); + wrapper.write(message); + }), + py::arg("flush") = py::cpp_function([] {})); + auto errorWrapper = make_python_type( + "MO2ErrorWrapper", py::make_tuple(), + py::arg("write") = py::cpp_function([](std::string_view message) { + static PrintWrapper wrapper(MOBase::log::Error); + wrapper.write(message); + }), + py::arg("flush") = py::cpp_function([] {})); + py::module_ sys = py::module_::import("sys"); + sys.attr("stdout") = printWrapper(); + sys.attr("stderr") = errorWrapper(); + + // this is required to handle exception in Python code OUTSIDE of pybind11 call, + // typically on Qt classes with methods overridden on the Python side + // + // without this, the application will crash instead of properly handling the + // exception as it would do with a py::error_already_set{} + // + // IMPORTANT: sys.attr("excepthook") = sys.attr("__excepthook__") DOES NOT WORK, + // and I have no clue why since the attribute does not seem to get updated (at + // least a print does not show it) + // + sys.attr("excepthook") = + py::eval("lambda x, y, z: sys.__excepthook__(x, y, z)"); + } + + // Small structure to hold the levels - There are copy paste from + // my Python version and I assume these will not change soon: + struct PyLogLevel { + static constexpr int CRITICAL = 50; + static constexpr int ERROR = 40; + static constexpr int WARNING = 30; + static constexpr int INFO = 20; + static constexpr int DEBUG = 10; + }; + + // This is the function we are going to use as our Handler .emit + // method. + void emit_function(py::object record) + { + + // There are other parameters that could be used, but this is minimal + // for now (filename, line number, etc.). + const int level = record.attr("levelno").cast(); + const std::wstring msg = py::str(record.attr("msg")).cast(); + + switch (level) { + case PyLogLevel::CRITICAL: + case PyLogLevel::ERROR: + MOBase::log::error("{}", msg); + break; + case PyLogLevel::WARNING: + MOBase::log::warn("{}", msg); + break; + case PyLogLevel::INFO: + MOBase::log::info("{}", msg); + break; + case PyLogLevel::DEBUG: + default: // There is a "NOTSET" level in theory: + MOBase::log::debug("{}", msg); + break; + } + }; + + void configure_python_logging(py::module_ mobase) + { + // most of this is dealing with actual Python objects since it is not + // possible to derive from logging.Handler in C++ using pybind11, + // and since a lot of this would require extra register only for this. + + // see also + // https://github.com/pybind/pybind11/issues/1193#issuecomment-429451094 + + // retrieve the logging module and the Handler class. + auto logging = py::module_::import("logging"); + auto Handler = logging.attr("Handler"); + + // create the "MO2Handler" python class + auto MO2Handler = + make_python_type("LogHandler", py::make_tuple(Handler), + py::arg("emit") = py::cpp_function(emit_function)); + + // create the default logger + auto handler = MO2Handler(); + handler.attr("setLevel")(PyLogLevel::DEBUG); + auto logger = logging.attr("getLogger")(py::object(mobase.attr("__name__"))); + logger.attr("setLevel")(PyLogLevel::DEBUG); + + // set mobase attributes + mobase.attr("LogHandler") = MO2Handler; + mobase.attr("logger") = logger; + + logging.attr("root").attr("setLevel")(PyLogLevel::DEBUG); + logging.attr("root").attr("addHandler")(handler); + } + +} // namespace mo2::python diff --git a/libs/plugin_python/src/runner/pythonutils.h b/libs/plugin_python/src/runner/pythonutils.h new file mode 100644 index 0000000..019e782 --- /dev/null +++ b/libs/plugin_python/src/runner/pythonutils.h @@ -0,0 +1,25 @@ +#ifndef PYTHONRUNNER_UTILS_H +#define PYTHONRUNNER_UTILS_H + +#include + +#include + +namespace mo2::python { + + /** + * @brief Configure Python stdout and stderr to log to MO2. + * + */ + void configure_python_stream(); + + /** + * @brief Configure logging for MO2 python plugin. + * + * @param mobase The mobase module. + */ + void configure_python_logging(pybind11::module_ mobase); + +} // namespace mo2::python + +#endif diff --git a/libs/plugin_python/tests/CMakeLists.txt b/libs/plugin_python/tests/CMakeLists.txt new file mode 100644 index 0000000..0141184 --- /dev/null +++ b/libs/plugin_python/tests/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.16) + +add_subdirectory(python) +add_subdirectory(runner) diff --git a/libs/plugin_python/tests/mocks/DummyFileTree.h b/libs/plugin_python/tests/mocks/DummyFileTree.h new file mode 100644 index 0000000..cf81691 --- /dev/null +++ b/libs/plugin_python/tests/mocks/DummyFileTree.h @@ -0,0 +1,34 @@ +#ifndef DUMMY_TREE_H +#define DUMMY_TREE_H + +#include + +// filetree implementation for testing purpose +// +class DummyFileTree : public MOBase::IFileTree { +public: + DummyFileTree(std::shared_ptr parent, QString name) + : FileTreeEntry(parent, name), IFileTree() + { + } + +protected: + std::shared_ptr makeDirectory(std::shared_ptr parent, + QString name) const override + { + return std::make_shared(parent, name); + } + + bool doPopulate(std::shared_ptr, + std::vector>&) const override + { + return true; + } + + std::shared_ptr doClone() const override + { + return std::make_shared(nullptr, name()); + } +}; + +#endif diff --git a/libs/plugin_python/tests/mocks/MockOrganizer.h b/libs/plugin_python/tests/mocks/MockOrganizer.h new file mode 100644 index 0000000..3ccfa3c --- /dev/null +++ b/libs/plugin_python/tests/mocks/MockOrganizer.h @@ -0,0 +1,65 @@ +#include +#include + +using namespace MOBase; + +class MockOrganizer : public IOrganizer { +public: + // clang-format off + MOCK_METHOD(IModRepositoryBridge*, createNexusBridge, (), (const, override)); + MOCK_METHOD(QString, instanceName, (), (const, override)); + MOCK_METHOD(QString, profileName, (), (const, override)); + MOCK_METHOD(QString, profilePath, (), (const, override)); + MOCK_METHOD(QString, downloadsPath, (), (const, override)); + MOCK_METHOD(QString, overwritePath, (), (const, override)); + MOCK_METHOD(QString, basePath, (), (const, override)); + MOCK_METHOD(QString, modsPath, (), (const, override)); + MOCK_METHOD(VersionInfo, appVersion, (), (const, override)); + MOCK_METHOD(Version, version, (), (const, override)); + MOCK_METHOD(IModInterface*, createMod, (GuessedValue &name), (override)); + MOCK_METHOD(IPluginGame*, getGame, (const QString &gameName), (const, override)); + MOCK_METHOD(void, modDataChanged, (IModInterface *mod), (override)); + MOCK_METHOD(QVariant, pluginSetting, (const QString &pluginName, const QString &key), (const, override)); + MOCK_METHOD(void, setPluginSetting, (const QString &pluginName, const QString &key, const QVariant &value), (override)); + MOCK_METHOD(bool, isPluginEnabled, (const QString& pluginName), (const, override)); + MOCK_METHOD(bool, isPluginEnabled, (IPlugin *plugin), (const, override)); + MOCK_METHOD(QVariant, persistent, (const QString &pluginName, const QString &key, const QVariant &def), (const, override)); + MOCK_METHOD(void, setPersistent, (const QString &pluginName, const QString &key, const QVariant &value, bool sync), (override)); + MOCK_METHOD(QString, pluginDataPath, (), (const, override)); + MOCK_METHOD(IModInterface*, installMod, (const QString &fileName, const QString &nameSuggestion), (override)); + MOCK_METHOD(QString, resolvePath, (const QString &fileName), (const, override)); + MOCK_METHOD(QStringList, listDirectories, (const QString &directoryName), (const, override)); + MOCK_METHOD(QStringList, findFiles, (const QString &path, const std::function &filter), (const, override)); + MOCK_METHOD(QStringList, findFiles, (const QString &path, const QStringList &filter), (const, override)); + MOCK_METHOD(QStringList, getFileOrigins, (const QString &fileName) ,(const, override)); + MOCK_METHOD(QList, findFileInfos, (const QString &path, const std::function &filter), (const, override)); + MOCK_METHOD(std::shared_ptr, virtualFileTree, (), (const, override)); + MOCK_METHOD(MOBase::IInstanceManager*, instanceManager, (), (const, override)); + MOCK_METHOD(MOBase::IDownloadManager*, downloadManager, (), (const, override)); + MOCK_METHOD(MOBase::IPluginList*, pluginList, (), (const, override)); + MOCK_METHOD(MOBase::IModList*, modList, (), (const, override)); + MOCK_METHOD(MOBase::IExecutablesList*, executablesList, (), (const, override)); + MOCK_METHOD(std::shared_ptr, profile, (), (const, override)); + MOCK_METHOD(QStringList, profileNames, (), (const, override)); + MOCK_METHOD(std::shared_ptr, getProfile, (const QString& name), (const, override)); + MOCK_METHOD(MOBase::IGameFeatures*, gameFeatures, (), (const, override)); + MOCK_METHOD(HANDLE, startApplication, (const QString &executable, const QStringList &args, const QString &cwd, const QString &profile, const QString &forcedCustomOverwrite, bool ignoreCustomOverwrite), (override)); + MOCK_METHOD(bool, waitForApplication, (HANDLE handle, bool refresh, LPDWORD exitCode), (const, override)); + MOCK_METHOD(bool, onAboutToRun, (const std::function &func), (override)); + MOCK_METHOD(bool, onAboutToRun, (const std::function &func), (override)); + MOCK_METHOD(bool, onFinishedRun, (const std::function &func), (override)); + MOCK_METHOD(void, refresh, (bool saveChanges), (override)); + MOCK_METHOD(MOBase::IPluginGame const *, managedGame, (), (const, override)); + MOCK_METHOD(bool, onUserInterfaceInitialized, (const std::function &), (override)); + MOCK_METHOD(bool, onNextRefresh, (const std::function&, bool), (override)); + MOCK_METHOD(bool, onProfileCreated, (const std::function&), (override)); + MOCK_METHOD(bool, onProfileRemoved, (const std::function&), (override)); + MOCK_METHOD(bool, onProfileRenamed, (const std::function&), (override)); + MOCK_METHOD(bool, onProfileChanged, (const std::function &), (override)); + MOCK_METHOD(bool, onPluginSettingChanged, (const std::function &), (override)); + MOCK_METHOD(bool, onPluginEnabled, (const std::function&), (override)); + MOCK_METHOD(bool, onPluginEnabled, (const QString&, const std::function&), (override)); + MOCK_METHOD(bool, onPluginDisabled, (const std::function&), (override)); + MOCK_METHOD(bool, onPluginDisabled, (const QString&, const std::function&), (override)); + // clang-format on +}; diff --git a/libs/plugin_python/tests/python/CMakeLists.txt b/libs/plugin_python/tests/python/CMakeLists.txt new file mode 100644 index 0000000..f473373 --- /dev/null +++ b/libs/plugin_python/tests/python/CMakeLists.txt @@ -0,0 +1,75 @@ +cmake_minimum_required(VERSION 3.16) + +# pytest +cmake_policy(SET CMP0144 NEW) + +find_package(mo2-uibase CONFIG REQUIRED) +find_package(GTest REQUIRED) + +set(PYLIB_DIR ${CMAKE_CURRENT_BINARY_DIR}/pylibs) + +set(UIBASE_PATH $) + +add_custom_target(python-tests) +target_sources(python-tests + PRIVATE + conftest.py + test_argument_wrapper.py + test_filetree.py + test_functional.py + test_guessed_string.py + test_organizer.py + test_path_wrappers.py + test_qt_widgets.py + test_qt.py + test_shared_cpp_owner.py +) + +add_test(NAME pytest + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/pylibs/bin/pytest.exe ${CMAKE_CURRENT_SOURCE_DIR} -s +) + +set_tests_properties(pytest + PROPERTIES + DEPENDS python-tests + ENVIRONMENT_MODIFICATION + "PYTHONPATH=set:${PYLIB_DIR}\\;$;\ +UIBASE_PATH=set:${UIBASE_PATH}" +) + +mo2_python_pip_install(python-tests + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pylibs + PACKAGES pytest + PyQt${MO2_QT_VERSION_MAJOR}==${MO2_PYQT_VERSION} + PyQt${MO2_QT_VERSION_MAJOR}-Qt${MO2_QT_VERSION_MAJOR}==${MO2_QT_VERSION}) +add_dependencies(python-tests mobase) +set_target_properties(python-tests PROPERTIES FOLDER tests/python) + +file(GLOB test_files CONFIGURE_DEPENDS "test_*.cpp") +foreach (test_file ${test_files}) + get_filename_component(target ${test_file} NAME_WLE) + + string(REPLACE "test_" "" pymodule ${target}) + pybind11_add_module(${target} EXCLUDE_FROM_ALL THIN_LTO ${test_file}) + set_target_properties(${target} + PROPERTIES + CXX_STANDARD 23 + OUTPUT_NAME ${pymodule} + FOLDER tests/python + LIBRARY_OUTPUT_DIRECTORY "${PYLIB_DIR}/mobase_tests") + + if(DEFINED CMAKE_CONFIGURATION_TYPES) + foreach(config ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER ${config} config) + set_target_properties(${target} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY_${config} "${PYLIB_DIR}/mobase_tests") + endforeach() + endif() + + target_link_libraries(${target} PRIVATE + mo2::uibase Qt6::Core Qt6::Widgets pybind11::qt pybind11::utils GTest::gmock) + + target_include_directories(${target} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../mocks) + add_dependencies(python-tests ${target}) +endforeach() diff --git a/libs/plugin_python/tests/python/conftest.py b/libs/plugin_python/tests/python/conftest.py new file mode 100644 index 0000000..99e7800 --- /dev/null +++ b/libs/plugin_python/tests/python/conftest.py @@ -0,0 +1,12 @@ +import os +import sys + +from PyQt6.QtWidgets import QApplication + + +def pytest_configure(): + global app + + os.add_dll_directory(str(os.getenv("UIBASE_PATH"))) + + app = QApplication(sys.argv) diff --git a/libs/plugin_python/tests/python/test_argument_wrapper.cpp b/libs/plugin_python/tests/python/test_argument_wrapper.cpp new file mode 100644 index 0000000..e87f516 --- /dev/null +++ b/libs/plugin_python/tests/python/test_argument_wrapper.cpp @@ -0,0 +1,61 @@ +#include "pybind11_utils/smart_variant_wrapper.h" + +#include +#include + +#include + +namespace mo2::python::detail { + + template <> + struct smart_variant_converter { + static std::string from(int const& value) { return std::to_string(value); } + }; + + template <> + struct smart_variant_converter { + static int from(std::string const& value) { return std::stoi(value); } + }; + +} // namespace mo2::python::detail + +// wrapper that can be constructed from +using Wrapper = mo2::python::smart_variant; + +template +auto wrap(Fn&& fn) +{ + return mo2::python::wrap_arguments(std::forward(fn)); +} + +std::string fn1(std::string const& value) +{ + return value + "-" + value; +} + +int fn2(int value) +{ + return value * 2; +} + +std::string fn3(int value, std::vector values, std::string const& name) +{ + return name + "-" + std::to_string(value + values.size()); +} + +PYBIND11_MODULE(argument_wrapper, m) +{ + m.def("fn1_raw", &fn1); + m.def("fn1_wrap", wrap(&fn1)); + m.def("fn1_wrap_0", wrap<0>(&fn1)); + + m.def("fn2_raw", &fn2); + m.def("fn2_wrap", wrap(&fn2)); + m.def("fn2_wrap_0", wrap<0>(&fn2)); + + m.def("fn3_raw", &fn3); + m.def("fn3_wrap", wrap(&fn3)); + m.def("fn3_wrap_0", wrap<0>(&fn3)); + m.def("fn3_wrap_2", wrap<2>(&fn3)); + m.def("fn3_wrap_0_2", wrap<0, 2>(&fn3)); +} diff --git a/libs/plugin_python/tests/python/test_argument_wrapper.py b/libs/plugin_python/tests/python/test_argument_wrapper.py new file mode 100644 index 0000000..08b4a1b --- /dev/null +++ b/libs/plugin_python/tests/python/test_argument_wrapper.py @@ -0,0 +1,62 @@ +import pytest + +import mobase_tests.argument_wrapper as m + + +def test_argument_wrapper_fn1(): + assert m.fn1_raw("hello") == "hello-hello" + + with pytest.raises(TypeError): + m.fn1_raw(1) # pyright: ignore[reportArgumentType] + + assert m.fn1_wrap("hello") == "hello-hello" + assert m.fn1_wrap(32) == "32-32" + + assert m.fn1_wrap_0("world") == "world-world" + assert m.fn1_wrap_0(45) == "45-45" + + +def test_argument_wrapper_fn2(): + assert m.fn2_raw(33) == 66 + + with pytest.raises(TypeError): + m.fn2_raw("12") # pyright: ignore[reportArgumentType] + + assert m.fn2_wrap("15") == 30 + assert m.fn2_wrap(32) == 64 + + assert m.fn2_wrap_0("-15") == -30 + assert m.fn2_wrap_0(45) == 90 + + +def test_argument_wrapper_fn3(): + assert m.fn3_raw(33, [], "hello") == "hello-33" + assert m.fn3_raw(33, [1, 2], "hello") == "hello-35" + + with pytest.raises(TypeError): + m.fn3_raw("12", [], "hello") # pyright: ignore[reportArgumentType] + + with pytest.raises(TypeError): + m.fn3_raw(36, [], 136) # pyright: ignore[reportArgumentType] + + assert m.fn3_wrap(14, [1, 2], "world") == "world-16" + assert m.fn3_wrap("15", [0], "woot") == "woot-16" + assert m.fn3_wrap(17, [], 33) == "33-17" + assert m.fn3_wrap("15", [], 44) == "44-15" + + assert m.fn3_wrap_0_2(14, [1, 2], "world") == "world-16" + assert m.fn3_wrap_0_2("15", [0], "woot") == "woot-16" + assert m.fn3_wrap_0_2(17, [], 33) == "33-17" + assert m.fn3_wrap_0_2("15", [], 44) == "44-15" + + assert m.fn3_wrap_0(14, [1, 2], "world") == "world-16" + assert m.fn3_wrap_0("15", [], "w00t") == "w00t-15" + + with pytest.raises(TypeError): + m.fn3_wrap_0(14, [], 12) # pyright: ignore[reportArgumentType] + + assert m.fn3_wrap_2(14, [1, 2], "world") == "world-16" + assert m.fn3_wrap_2(15, [], 18) == "18-15" + + with pytest.raises(TypeError): + m.fn3_wrap_2("14", [], 12) # pyright: ignore[reportArgumentType] diff --git a/libs/plugin_python/tests/python/test_filetree.cpp b/libs/plugin_python/tests/python/test_filetree.cpp new file mode 100644 index 0000000..cce43cf --- /dev/null +++ b/libs/plugin_python/tests/python/test_filetree.cpp @@ -0,0 +1,22 @@ +#include "pybind11_qt/pybind11_qt.h" + +#include +#include + +#include + +using namespace MOBase; + +PYBIND11_MODULE(filetree, m) +{ + // test the FileTypes stuff + m.def("is_file", [](IFileTree::FileTypes const& t) { + return t.testFlag(IFileTree::FILE); + }); + m.def("is_directory", [](IFileTree::FileTypes const& t) { + return t.testFlag(IFileTree::DIRECTORY); + }); + m.def("value", [](IFileTree::FileTypes const& t) { + return t.toInt(); + }); +} diff --git a/libs/plugin_python/tests/python/test_filetree.py b/libs/plugin_python/tests/python/test_filetree.py new file mode 100644 index 0000000..0322faa --- /dev/null +++ b/libs/plugin_python/tests/python/test_filetree.py @@ -0,0 +1,86 @@ +from typing import TypeAlias, cast + +import mobase + +import mobase_tests.filetree as m + + +def test_filetype(): + FT = mobase.FileTreeEntry.FileTypes + + assert mobase.FileTreeEntry.FILE == FT.FILE + assert m.is_file(FT.FILE) + assert not m.is_directory(FT.FILE) + assert m.value(FT.FILE) == FT.FILE.value + + assert mobase.FileTreeEntry.DIRECTORY == FT.DIRECTORY + assert not m.is_file(FT.DIRECTORY) + assert m.is_directory(FT.DIRECTORY) + assert m.value(FT.DIRECTORY) == FT.DIRECTORY.value + + assert mobase.FileTreeEntry.FILE_OR_DIRECTORY == FT.FILE_OR_DIRECTORY + assert m.is_file(FT.FILE_OR_DIRECTORY) + assert m.is_directory(FT.FILE_OR_DIRECTORY) + assert m.value(FT.FILE_OR_DIRECTORY) == FT.FILE_OR_DIRECTORY.value + + assert FT.FILE_OR_DIRECTORY == FT.FILE | FT.DIRECTORY + assert m.is_file(FT.FILE | FT.DIRECTORY) + assert m.is_directory(FT.FILE | FT.DIRECTORY) + assert m.value(FT.FILE | FT.DIRECTORY) == (FT.FILE.value | FT.DIRECTORY.value) + + assert m.is_file(FT.FILE_OR_DIRECTORY & FT.FILE) + assert not m.is_directory(FT.FILE_OR_DIRECTORY & FT.FILE) + + assert not m.is_file(FT.FILE_OR_DIRECTORY & FT.DIRECTORY) + assert m.is_directory(FT.FILE_OR_DIRECTORY & FT.DIRECTORY) + + assert m.is_file(FT.FILE_OR_DIRECTORY & ~FT.DIRECTORY) + assert not m.is_directory(FT.FILE_OR_DIRECTORY & ~FT.DIRECTORY) + + +_tree_values: TypeAlias = list["str | tuple[str, _tree_values]"] + + +def make_tree( + values: _tree_values, root: mobase.IFileTree | None = None +) -> mobase.IFileTree: + if root is None: + root = cast(mobase.IFileTree, mobase.private.makeTree()) # type: ignore + + for value in values: + if isinstance(value, str): + root.addFile(value) + else: + sub_tree = root.addDirectory(value[0]) + make_tree(value[1], sub_tree) + + return root + + +def test_walk(): + tree = make_tree( + [("a", []), ("b", ["u", "v"]), "c.x", "d.y", ("e", [("q", ["c.t", ("p", [])])])] + ) + + assert {"a", "b", "b/u", "b/v", "c.x", "d.y", "e", "e/q", "e/q/c.t", "e/q/p"} == { + e.path("/") for e in tree.walk() + } + + entries: list[str] = [] + for e in tree.walk(): + if e.name() == "e": + break + entries.append(e.path("/")) + assert {"a", "b", "b/u", "b/v"} == set(entries) + + +def test_glob(): + tree = make_tree( + [("a", []), ("b", ["u", "v"]), "c.x", "d.y", ("e", [("q", ["c.t", ("p", [])])])] + ) + + assert {"a", "b", "b/u", "b/v", "c.x", "d.y", "e", "e/q", "e/q/c.t", "e/q/p"} == { + e.path("/") for e in tree.glob("**/*") + } + + assert {"d.y"} == {e.path("/") for e in tree.glob("**/*.y")} diff --git a/libs/plugin_python/tests/python/test_functional.cpp b/libs/plugin_python/tests/python/test_functional.cpp new file mode 100644 index 0000000..4858516 --- /dev/null +++ b/libs/plugin_python/tests/python/test_functional.cpp @@ -0,0 +1,38 @@ +#include "pybind11_utils/functional.h" + +#include + +PYBIND11_MODULE(functional, m) +{ + m.def("fn_0_arg", [](std::function const& fn) { + return fn(); + }); + + m.def("fn_1_arg", [](std::function const& fn, int a) { + return fn(a); + }); + + m.def("fn_2_arg", [](std::function const& fn, int a, int b) { + return fn(a, b); + }); + + m.def("fn_0_or_1_arg", [](std::function const& fn) { + return fn(); + }); + + m.def("fn_0_or_1_arg", [](std::function const& fn) { + return fn(1); + }); + + m.def("fn_1_or_2_or_3_arg", [](std::function const& fn) { + return fn(1); + }); + + m.def("fn_1_or_2_or_3_arg", [](std::function const& fn) { + return fn(1, 2); + }); + + m.def("fn_1_or_2_or_3_arg", [](std::function const& fn) { + return fn(1, 2, 3); + }); +} diff --git a/libs/plugin_python/tests/python/test_functional.py b/libs/plugin_python/tests/python/test_functional.py new file mode 100644 index 0000000..f8da087 --- /dev/null +++ b/libs/plugin_python/tests/python/test_functional.py @@ -0,0 +1,55 @@ +from typing import Any + +import pytest + +import mobase_tests.functional as m + + +def test_guessed_string(): + # available functions: + # - fn_0_arg, fn_1_arg, fn_2_arg + # - fn_0_or_1_arg, fn_1_or_2_or_3_arg + + def no_args() -> int: + return 0 + + def len_of_args(*args: int) -> int: + return len(args) + + def len_of_args_tweaked(x: int, *args: int, **kwargs: Any) -> int: + return x + len(args) + + def sum_of_args(*args: int) -> int: + return sum(args) + + assert m.fn_0_arg(lambda: 0) == 0 + assert m.fn_0_arg(lambda: 5) == 5 + assert m.fn_0_arg(lambda x=2: x) == 2 + assert m.fn_0_arg(len_of_args) == 0 + + assert m.fn_1_arg(lambda x: x, 4) == 4 + assert m.fn_1_arg(sum_of_args, 8) == 8 + assert m.fn_1_arg(lambda x=2, y=4: x + y, 3) == 7 + assert m.fn_1_arg(len_of_args_tweaked, 5) == 5 + + assert m.fn_2_arg(lambda x, y: x * y, 4, 5) == 20 + assert m.fn_2_arg(lambda x, y=3: x * y, 4, 2) == 8 + assert m.fn_2_arg(sum_of_args, 8, 9) == 17 + assert m.fn_2_arg(lambda x=2, y=4: x + y, 3, 3) == 6 + assert m.fn_2_arg(len_of_args_tweaked, 5, 2) == 6 + + assert m.fn_0_or_1_arg(lambda: 3) == 3 + assert m.fn_0_or_1_arg(lambda x: x) == 1 + + # the 0 arg is bound first, both are possible, the 0 arg is chosen + assert m.fn_0_or_1_arg(lambda x=2: x) == 2 + + with pytest.raises(TypeError): + m.fn_1_or_2_or_3_arg(no_args) # pyright: ignore[reportArgumentType, reportCallIssue] + + assert m.fn_1_or_2_or_3_arg(lambda x=4: x) == 1 + assert m.fn_1_or_2_or_3_arg(lambda x, y: x + y) == 3 # 1 + 2 + assert m.fn_1_or_2_or_3_arg(lambda x, y, z: x * y * z) == 6 # 1 * 2 * 3 + + # the 1 arg is bound first + assert m.fn_1_or_2_or_3_arg(sum_of_args) == 1 diff --git a/libs/plugin_python/tests/python/test_guessed_string.cpp b/libs/plugin_python/tests/python/test_guessed_string.cpp new file mode 100644 index 0000000..5e76773 --- /dev/null +++ b/libs/plugin_python/tests/python/test_guessed_string.cpp @@ -0,0 +1,34 @@ +#include "pybind11_qt/pybind11_qt.h" + +#include +#include + +#include + +using namespace MOBase; + +PYBIND11_MODULE(guessed_string, m) +{ + m.def("get_value", [](GuessedValue const& value) { + return value.operator const QString&(); + }); + m.def("get_variants", [](GuessedValue const& value) { + return value.variants(); + }); + + m.def("set_from_callback", + [](GuessedValue& value, + std::function&)> const& fn) { + fn(value); + }); + + // note: the function needs to take the guessed string by pointer if constructed + // from C++, this is to be taken into account when calling Python function (cf. + // installers) + m.def("get_from_callback", + [](std::function*)> const& fn) { + GuessedValue value; + fn(&value); + return (QString)value; + }); +} diff --git a/libs/plugin_python/tests/python/test_guessed_string.py b/libs/plugin_python/tests/python/test_guessed_string.py new file mode 100644 index 0000000..204e1f1 --- /dev/null +++ b/libs/plugin_python/tests/python/test_guessed_string.py @@ -0,0 +1,36 @@ +import mobase + +import mobase_tests.guessed_string as m + + +def test_guessed_string(): + # empty string + gs = mobase.GuessedString() + + assert len(gs.variants()) == 0 + assert not gs.variants() + assert str(gs) == "" + + # automatic conversion from string + assert m.get_value("test") == "test" + assert m.get_variants("test") == {"test"} + + # update + gs = mobase.GuessedString("fallback", mobase.GuessQuality.FALLBACK) + assert str(gs) == "fallback" + + gs.update("good", mobase.GuessQuality.GOOD) + assert str(gs) == "good" + assert gs.variants() == {"fallback", "good"} + + # back-and-forth + gs = mobase.GuessedString() + assert str(gs) == "" + + def _update(gs: mobase.GuessedString): + gs.update("test") + + m.set_from_callback(gs, _update) + assert str(gs) == "test" + + assert m.get_from_callback(_update) == "test" diff --git a/libs/plugin_python/tests/python/test_organizer.cpp b/libs/plugin_python/tests/python/test_organizer.cpp new file mode 100644 index 0000000..ea54c0d --- /dev/null +++ b/libs/plugin_python/tests/python/test_organizer.cpp @@ -0,0 +1,46 @@ +#include "pybind11_qt/pybind11_qt.h" + +#include + +#include +#include + +#include "MockOrganizer.h" + +namespace py = pybind11; +using namespace pybind11::literals; +using ::testing::NiceMock; + +PYBIND11_MODULE(organizer, m) +{ + using ::testing::_; + using ::testing::Eq; + using ::testing::Return; + + m.def("organizer", []() -> IOrganizer* { + MockOrganizer* mock = new NiceMock(); + ON_CALL(*mock, profileName).WillByDefault([&mock]() { + return "profile"; + }); + + const auto handle = (HANDLE)std::uintptr_t{4654}; + ON_CALL(*mock, startApplication) + .WillByDefault([handle](const auto& name, auto&&... args) { + return name == "valid.exe" ? handle : INVALID_HANDLE_VALUE; + }); + ON_CALL(*mock, waitForApplication) + .WillByDefault([&mock, original_handle = handle](HANDLE handle, bool, + LPDWORD exitCode) { + if (handle == original_handle) { + *exitCode = 0; + return true; + } + else { + *exitCode = static_cast(-1); + return false; + } + }); + + return mock; + }); +} diff --git a/libs/plugin_python/tests/python/test_organizer.py b/libs/plugin_python/tests/python/test_organizer.py new file mode 100644 index 0000000..3ac42d2 --- /dev/null +++ b/libs/plugin_python/tests/python/test_organizer.py @@ -0,0 +1,14 @@ +import pytest + +import mobase + +m = pytest.importorskip("mobase_tests.organizer") + + +def test_getters(): + o: mobase.IOrganizer = m.organizer() + assert o.profileName() == "profile" + assert o.startApplication("valid.exe") == 4654 + assert o.startApplication("invalid.exe") == mobase.INVALID_HANDLE_VALUE + assert o.waitForApplication(42) == (False, -1) + assert o.waitForApplication(4654) == (True, 0) diff --git a/libs/plugin_python/tests/python/test_path_wrappers.py b/libs/plugin_python/tests/python/test_path_wrappers.py new file mode 100644 index 0000000..5a0d701 --- /dev/null +++ b/libs/plugin_python/tests/python/test_path_wrappers.py @@ -0,0 +1,45 @@ +import sys +from pathlib import Path + +import pytest +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + + +def test_filepath_wrappers(): + # TBC that this works everywhere + version = ".".join(map(str, sys.version_info[:3])) + + # from string, ok + assert mobase.getProductVersion(sys.executable) == version + + # from path, ok + assert mobase.getProductVersion(Path(sys.executable)) == version + + # from QDir, ko + with pytest.raises(TypeError): + mobase.getProductVersion(QDir(sys.executable)) # pyright: ignore[reportArgumentType] + + +def test_executableinfo(): + info = mobase.ExecutableInfo("exe", QFileInfo(sys.executable)) + assert info.binary() == QFileInfo(sys.executable) + + info = mobase.ExecutableInfo("exe", sys.executable) + assert info.binary() == QFileInfo(sys.executable) + + info = mobase.ExecutableInfo("exe", Path(sys.executable)) + assert info.binary() == QFileInfo(sys.executable) + + info.withWorkingDirectory(Path(__file__).parent) + assert info.workingDirectory() == QFileInfo(__file__).dir() + + info.withWorkingDirectory(".") + assert info.workingDirectory() == QDir(".") + + info.withWorkingDirectory(Path(".")) + assert info.workingDirectory() == QDir(".") + + info.withWorkingDirectory(".") + assert info.workingDirectory() == QDir(".") diff --git a/libs/plugin_python/tests/python/test_qt.cpp b/libs/plugin_python/tests/python/test_qt.cpp new file mode 100644 index 0000000..5ef1f00 --- /dev/null +++ b/libs/plugin_python/tests/python/test_qt.cpp @@ -0,0 +1,155 @@ +#include "pybind11_qt/pybind11_qt.h" + +#include + +#include + +namespace py = pybind11; +using namespace pybind11::literals; + +PYBIND11_MODULE(qt, m) +{ + // QString + + m.def("create_qstring_with_emoji", []() { + return QString::fromUtf16(u"\U0001F600"); + }); + + m.def("consume_qstring_with_emoji", [](QString const& qstring) { + return qstring.length(); + }); + + m.def("qstring_to_stdstring", [](QString const& qstring) { + return qstring.toStdString(); + }); + + m.def("stdstring_to_qstring", [](std::string const& string) { + return QString::fromStdString(string); + }); + m.def("qstring_to_int", [](QString const& qstring) { + return qstring.toInt(); + }); + m.def("int_to_qstring", [](int value) { + return QString::number(value); + }); + + // QStringList + + m.def("qstringlist_join", [](QStringList const& values, QString const& sep) { + return values.join(sep); + }); + + m.def("qstringlist_at", [](QStringList const& values, int index) { + return values.at(index); + }); + + // QMap + + m.def("qmap_to_length", [](QMap const& map) { + QMap res; + for (auto it = map.begin(); it != map.end(); ++it) { + res[it.key()] = it.value().size(); + } + return res; + }); + + // QDateTime + + m.def( + "datetime_from_string", + [](QString const& date, Qt::DateFormat format) { + return QDateTime::fromString(date, format); + }, + "string"_a, "format"_a = Qt::DateFormat::ISODate); + + m.def( + "datetime_to_string", + [](QDateTime const& datetime, Qt::DateFormat format) { + return datetime.toString(format); + }, + "datetime"_a, "format"_a = Qt::DateFormat::ISODate); + + // QVariant + + m.def("qvariant_from_none", [](QVariant const& variant) { + return std::make_tuple(variant.userType() == QMetaType::UnknownType, + variant.isValid()); + }); + m.def("qvariant_from_int", [](QVariant const& variant) { + return std::make_tuple(variant.userType() == QMetaType::Int, variant.toInt()); + }); + m.def("qvariant_from_bool", [](QVariant const& variant) { + return std::make_tuple(variant.userType() == QMetaType::Bool, variant.toBool()); + }); + m.def("qvariant_from_str", [](QVariant const& variant) { + return std::make_tuple(variant.userType() == QMetaType::QString, + variant.toString()); + }); + m.def("qvariant_from_list", [](QVariant const& variant) { + return std::make_tuple(variant.userType() == QMetaType::QVariantList, + variant.toList()); + }); + m.def("qvariant_from_map", [](QVariant const& variant) { + return std::make_tuple(variant.userType() == QMetaType::QVariantMap, + variant.toMap()); + }); + + m.def("qvariant_none", []() { + return QVariant(); + }); + m.def("qvariant_int", []() { + return QVariant(42); + }); + m.def("qvariant_bool", []() { + return QVariant(true); + }); + m.def("qvariant_str", []() { + return QVariant("hello world"); + }); + m.def("qvariant_list", [] { + QVariantMap subMap; + subMap["bar"] = 42; + subMap["moo"] = QVariantList{44, true}; + QVariantList list; + list.push_back(33); + list.push_back(QVariantList{4, "foo"}); + list.push_back(false); + list.push_back("hello"); + list.push_back(QVariant()); + list.push_back(subMap); + list.push_back(45); + return QVariant(list); + }); + m.def("qvariant_map", []() { + QVariantMap map; + map["bar"] = 42; + map["moo"] = true; + map["baz"] = "world hello"; + return map; + }); + + // QFlags + + enum SimpleEnum { Value0 = 0x1, Value1 = 0x2, Value2 = 0x4 }; + Q_DECLARE_FLAGS(SimpleEnumFlags, SimpleEnum); + + py::enum_(m, "SimpleEnum", py::arithmetic()) + .value("Value0", Value0) + .value("Value1", Value1) + .value("Value2", Value2); + + m.def("qflags_explode", [](SimpleEnumFlags const& flags) { + return std::make_tuple(flags.toInt(), flags.testFlag(Value0), + flags.testFlag(Value1), flags.testFlag(Value2)); + }); + m.def("qflags_create", [](bool v0, bool v1, bool v2) { + SimpleEnumFlags r; + if (v0) + r.setFlag(Value0); + if (v1) + r.setFlag(Value1); + if (v2) + r.setFlag(Value2); + return r; + }); +} diff --git a/libs/plugin_python/tests/python/test_qt.py b/libs/plugin_python/tests/python/test_qt.py new file mode 100644 index 0000000..991e33c --- /dev/null +++ b/libs/plugin_python/tests/python/test_qt.py @@ -0,0 +1,119 @@ +from PyQt6.QtCore import QDateTime, Qt + +import mobase_tests.qt as m + + +def test_qstring(): + assert m.qstring_to_stdstring("test") == "test" + assert m.stdstring_to_qstring("test") == "test" + + assert m.qstring_to_stdstring("éàüö") == "éàüö" + assert m.stdstring_to_qstring("éàüö") == "éàüö" + assert m.qstring_to_stdstring("خالد") == "خالد" + assert m.qstring_to_stdstring("🌎") == "🌎" + + assert m.qstring_to_int("2") == 2 + assert m.int_to_qstring(2) == "2" + + emoji = m.create_qstring_with_emoji() + + assert emoji.encode("utf-16be", "surrogatepass") == b"\xd8\x3d\xde\x00" + assert m.consume_qstring_with_emoji(emoji) == 2 + + assert m.consume_qstring_with_emoji("🌎") == 2 + + +def test_qstringlist(): + assert m.qstringlist_join([""], "--") == "" + assert m.qstringlist_join(["a", "b"], "") == "ab" + assert m.qstringlist_join(["x", "y"], ";") == "x;y" + + assert m.qstringlist_at(["x", "y"], 0) == "x" + assert m.qstringlist_at(["x", "y"], 1) == "y" + + +def test_qmap(): + assert m.qmap_to_length({"t1": "abc", "t2": "o", "t3": ""}) == { + "t1": 3, + "t2": 1, + "t3": 0, + } + + +def test_qdatetime(): + assert m.datetime_from_string("2022-03-01") == QDateTime(2022, 3, 1, 0, 0) + + date = QDateTime(1995, 5, 20, 0, 0) + assert ( + m.datetime_from_string( + date.toString(Qt.DateFormat.TextDate), Qt.DateFormat.TextDate + ) + == date + ) + + assert m.datetime_to_string(date) == "1995-05-20T00:00:00" + assert m.datetime_to_string(date, Qt.DateFormat.TextDate) == date.toString( + Qt.DateFormat.TextDate + ) + + +def test_qvariant(): + # Python -> C++ + + assert m.qvariant_from_none(None) == (True, False) + + assert m.qvariant_from_int(-52) == (True, -52) + assert m.qvariant_from_int(0) == (True, 0) + assert m.qvariant_from_int(33) == (True, 33) + + assert m.qvariant_from_bool(True) == (True, True) + assert m.qvariant_from_bool(False) == (True, False) + + assert m.qvariant_from_str("a string") == (True, "a string") + + assert m.qvariant_from_list([]) == (True, []) + assert m.qvariant_from_list([1, "hello", False]) == (True, [1, "hello", False]) + + assert m.qvariant_from_map({"a": 33, "b": False, "c": ["a", "b"]}) == ( + True, + {"a": 33, "b": False, "c": ["a", "b"]}, + ) + + # C++ -> Python (see .cpp file for the value) + + assert m.qvariant_none() is None + assert m.qvariant_int() == 42 + assert m.qvariant_bool() is True + assert m.qvariant_str() == "hello world" + + assert m.qvariant_map() == {"baz": "world hello", "bar": 42, "moo": True} + + assert m.qvariant_list() == [ + 33, + [4, "foo"], + False, + "hello", + None, + {"bar": 42, "moo": [44, True]}, + 45, + ] + + +def test_qflags(): + v0, v1, v2 = m.SimpleEnum.Value0, m.SimpleEnum.Value1, m.SimpleEnum.Value2 + + assert m.qflags_explode(v0 | v1) == (0x3, True, True, False) + assert m.qflags_explode(v0 | v2) == (0x5, True, False, True) + assert m.qflags_explode(0) == (0, False, False, False) + + assert not (m.qflags_create(False, False, False) & v0) + assert not (m.qflags_create(False, False, False) & v1) + assert not (m.qflags_create(False, False, False) & v2) + + assert m.qflags_create(True, False, False) & v0 + assert m.qflags_create(True, True, False) & v0 + assert m.qflags_create(True, True, False) & v1 + assert not (m.qflags_create(True, True, False) & v2) + + assert m.qflags_create(True, False, False) | v0 == v0 + assert m.qflags_create(True, False, False) | v0 | v2 == v0 | v2 diff --git a/libs/plugin_python/tests/python/test_qt_widgets.cpp b/libs/plugin_python/tests/python/test_qt_widgets.cpp new file mode 100644 index 0000000..6d8a901 --- /dev/null +++ b/libs/plugin_python/tests/python/test_qt_widgets.cpp @@ -0,0 +1,85 @@ +#include "pybind11_qt/pybind11_qt.h" + +#include + +#include +#include + +namespace py = pybind11; +using namespace pybind11::literals; + +QMap s_Widgets; +QWidget* s_Parent; + +class CustomWidget : public QWidget { +public: + CustomWidget(QString const& name, QWidget* parent = nullptr) : QWidget(parent) + { + s_Widgets[name] = this; + setProperty("name", name); + } + + ~CustomWidget() { s_Widgets.remove(property("name").toString()); } +}; + +class PyCustomWidget : public CustomWidget { +public: + using CustomWidget::CustomWidget; + int heightForWidth(int value) const + { + PYBIND11_OVERRIDE(int, CustomWidget, heightForWidth, value); + } +}; + +PYBIND11_MODULE(qt_widgets, m) +{ + s_Parent = new QWidget(); + + py::class_> + pyCustomWidget(m, "CustomWidget"); + pyCustomWidget + .def(py::init(), "name"_a, "parent"_a = (QWidget*)nullptr) + .def("set_parent_cpp", [](CustomWidget* w) { + w->setParent(s_Parent); + }); + py::qt::add_qt_delegate(pyCustomWidget, "_widget"); + + m.def("is_alive", [](QString const& name) { + return s_Widgets.contains(name); + }); + + m.def("get", [](QString const& name) { + return s_Widgets.contains(name) ? s_Widgets[name] : nullptr; + }); + + m.def("set_parent", [](QWidget* widget) { + widget->setParent(s_Parent); + }); + + m.def("is_owned_cpp", [](QString const& name) { + return s_Widgets.contains(name) && s_Widgets[name]->parent() == s_Parent; + }); + + m.def("make_widget_own_cpp", [](QString const& name) -> QWidget* { + return new CustomWidget(name, s_Parent); + }); + + m.def( + "make_widget_own_py", + [](QString const& name) -> QWidget* { + return new CustomWidget(name); + }, + py::return_value_policy::take_ownership); + + // simply passing the widget gives the ownership of the Python object to C++ + m.def("send_to_cpp", [](QString const& name, QWidget* widget) { + widget->setProperty("name", name); + widget->setParent(s_Parent); + s_Widgets[name] = widget; + }); + + m.def("heightForWidth", [](QString const& name, int value) { + return s_Widgets.contains(name) ? s_Widgets[name]->heightForWidth(value) + : -1024; + }); +} diff --git a/libs/plugin_python/tests/python/test_qt_widgets.py b/libs/plugin_python/tests/python/test_qt_widgets.py new file mode 100644 index 0000000..057f34e --- /dev/null +++ b/libs/plugin_python/tests/python/test_qt_widgets.py @@ -0,0 +1,62 @@ +from PyQt6.QtWidgets import QWidget + +import mobase_tests.qt_widgets as m + + +class PyWidget(QWidget): + def heightForWidth(self, a0: int) -> int: + return a0 * 3 + 4 + + +class PyCustomWidget(m.CustomWidget): + def __init__(self, name: str): + super().__init__(name) + + def heightForWidth(self, value: int) -> int: + return value * 6 - 5 + + +def test_qt_widget(): + # own cpp + w = m.make_widget_own_cpp("w1") + assert m.is_alive("w1") + assert m.is_owned_cpp("w1") + + del w + assert m.is_alive("w1") + + # own py + w = m.make_widget_own_py("w2") + assert m.is_alive("w2") + assert not m.is_owned_cpp("w2") + + del w + assert not m.is_alive("w2") + + # transfer to C++ + w = PyWidget() + m.send_to_cpp("w3", w) + + # delete the reference w - this should NOT delete the underlying object since it + # was transferred to C++ + del w + assert m.is_alive("w3") + assert m.is_owned_cpp("w3") + + # if the Python object is dead (BAD!), this will crash horrible + assert m.heightForWidth("w3", 4) == 4 * 3 + 4 + + # CustomWidget as a qholder, so the construction itself transfers the ownership + # to C++ + w = PyCustomWidget("w4") + w.set_parent_cpp() + assert m.is_alive("w4") + assert m.heightForWidth("w4", 7) == 6 * 7 - 5 + assert w.heightForWidth(7) == 6 * 7 - 5 + + # can call function not defined and not bound through the delegate + assert not w.hasHeightForWidth() + + del w + assert m.is_alive("w4") + assert m.heightForWidth("w4", 7) == 6 * 7 - 5 diff --git a/libs/plugin_python/tests/python/test_shared_cpp_owner.cpp b/libs/plugin_python/tests/python/test_shared_cpp_owner.cpp new file mode 100644 index 0000000..f24cafd --- /dev/null +++ b/libs/plugin_python/tests/python/test_shared_cpp_owner.cpp @@ -0,0 +1,67 @@ +#include "pybind11_utils/shared_cpp_owner.h" + +#include + +#include +#include + +namespace py = pybind11; +using namespace pybind11::literals; + +class Base; +static std::unordered_map bases; + +class Base { + std::string name_; + +public: + Base(std::string const& name) : name_{name} { bases[name] = this; } + virtual std::string fn() const = 0; + virtual ~Base() { bases.erase(name_); } +}; + +MO2_PYBIND11_SHARED_CPP_HOLDER(Base); + +class CppBase : public Base { +public: + using Base::Base; + std::string fn() const override { return "CppBase::fn()"; } +}; + +class PyBase : public Base { +public: + using Base::Base; + std::string fn() const override { PYBIND11_OVERRIDE_PURE(std::string, Base, fn, ); } +}; + +PYBIND11_MODULE(shared_cpp_owner, m) +{ + static std::shared_ptr base_ptr; + + py::class_>(m, "Base") + .def(py::init()) + .def("fn", &Base::fn); + + m.def("is_alive", [](std::string const& name) { + return bases.find(name) != bases.end(); + }); + + m.def("create", [](std::string const& name) -> std::shared_ptr { + return std::make_shared(name); + }); + m.def("create_and_store", [](std::string const& name) { + base_ptr = std::make_shared(name); + return base_ptr; + }); + m.def("store", [](std::shared_ptr ptr) { + base_ptr = ptr; + }); + m.def("clear", []() { + base_ptr.reset(); + }); + + m.def("call_fn", [](std::string const& name) { + auto it = bases.find(name); + return it != bases.end() ? it->second->fn() : ""; + }); +} diff --git a/libs/plugin_python/tests/python/test_shared_cpp_owner.py b/libs/plugin_python/tests/python/test_shared_cpp_owner.py new file mode 100644 index 0000000..0bcecf7 --- /dev/null +++ b/libs/plugin_python/tests/python/test_shared_cpp_owner.py @@ -0,0 +1,72 @@ +import mobase_tests.shared_cpp_owner as m + + +class PyBase(m.Base): + def __init__(self, name: str, value: int): + super().__init__(name) + self.value = value + + def fn(self): + return f"PyBase.fn({self.value})" + + +def test_shared_cpp_owner_1(): + # create from C++, owned by Python + + # create from C++ + p = m.create("tmp") + assert m.is_alive("tmp") + + # should delete since it's not owner by C++ + del p + assert not m.is_alive("tmp") + + +def test_shared_cpp_owner_2(): + # create from C++, owned by C++ (and Python) + + # create from C++ + p = m.create_and_store("tmp") + assert m.is_alive("tmp") + + # should not delete since it's owned by both C++ and Python + del p + assert m.is_alive("tmp") + + # clear from C++ should free it + m.clear() + assert not m.is_alive("tmp") + + +def test_shared_cpp_owner_3(): + # create from Python, owned by Python + + p = PyBase("foo", 1) + assert m.is_alive("foo") + assert m.call_fn("foo") == "PyBase.fn(1)" + + del p + assert not m.is_alive("foo") + + +def test_shared_cpp_owner_4(): + # create from Python, owned by C++ + + p = PyBase("foo", 2) + assert m.is_alive("foo") + + # send to C++ + m.store(p) + assert m.is_alive("foo") + assert m.call_fn("foo") == "PyBase.fn(2)" + + # delete in Python, should still be alived + del p + assert m.is_alive("foo") + + # should still be able to call fn() + assert m.call_fn("foo") == "PyBase.fn(2)" + + # clear in C++, should kill Python + m.clear() + assert not m.is_alive("foo") diff --git a/libs/plugin_python/tests/runner/CMakeLists.txt b/libs/plugin_python/tests/runner/CMakeLists.txt new file mode 100644 index 0000000..ea17438 --- /dev/null +++ b/libs/plugin_python/tests/runner/CMakeLists.txt @@ -0,0 +1,65 @@ +cmake_minimum_required(VERSION 3.22) + +# setting-up the tests for the runner is a bit complex because we need a tons of +# things + +# first we configure the tests as with other tests +add_executable(runner-tests EXCLUDE_FROM_ALL) +mo2_default_source_group() +mo2_target_sources(runner-tests + FOLDER src + PRIVATE + test_diagnose.cpp + test_filemapper.cpp + test_game.cpp + test_installer.cpp + test_iplugin.cpp + test_lifetime.cpp +) +mo2_target_sources(runner-tests + FOLDER src/mocks + PRIVATE + ../mocks/DummyFileTree.h + ../mocks/MockOrganizer.h +) +mo2_target_sources(runner-tests + FOLDER src/plugins + PRIVATE + plugins/dummy-diagnose.py + plugins/dummy-filemapper.py + plugins/dummy-game.py + plugins/dummy-installer.py + plugins/dummy-iplugin.py +) +mo2_configure_tests(runner-tests NO_SOURCES WARNINGS 4) + +set_target_properties(runner-tests PROPERTIES FOLDER tests/runner) + +# link to runner +target_link_libraries(runner-tests PUBLIC runner) + +# linking to Python - this is not required to get proper linking but required so that +# CMake generator variables will lookup appropriate DLLs for Python and update PATH +# accordingly thanks to mo2_configure_tests +target_link_libraries(runner-tests PUBLIC Python::Python) + +# add mocks +target_include_directories(runner-tests + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../mocks) + +set(PYLIB_DIR ${CMAKE_CURRENT_BINARY_DIR}/pylibs) +mo2_python_pip_install(runner-tests + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pylibs + PACKAGES + pytest + PyQt${MO2_QT_VERSION_MAJOR}==${MO2_PYQT_VERSION} + PyQt${MO2_QT_VERSION_MAJOR}-Qt${MO2_QT_VERSION_MAJOR}==${MO2_QT_VERSION}) + +add_dependencies(runner-tests mobase) + +set(PYTHONPATH "${PYLIB_DIR}\\;$\\;${Python_DLL_DIR}\\;${Python_LIB_DIR}") + +set_tests_properties(${runner-tests_gtests} + PROPERTIES + ENVIRONMENT "PLUGIN_DIR=${CMAKE_CURRENT_SOURCE_DIR}/plugins;PYTHONPATH=${PYTHONPATH}" +) diff --git a/libs/plugin_python/tests/runner/plugins/dummy-diagnose.py b/libs/plugin_python/tests/runner/plugins/dummy-diagnose.py new file mode 100644 index 0000000..6f25ef0 --- /dev/null +++ b/libs/plugin_python/tests/runner/plugins/dummy-diagnose.py @@ -0,0 +1,37 @@ +import mobase + + +class DummyDiagnose(mobase.IPluginDiagnose): + def activeProblems(self) -> list[int]: + return [1, 2] + + def shortDescription(self, key: int) -> str: + return f"short-{key}" + + def fullDescription(self, key: int) -> str: + return f"long-{key}" + + def hasGuidedFix(self, key: int) -> bool: + return key == 1 + + +class DummyDiagnoseAndGame(mobase.IPluginDiagnose, mobase.IPluginGame): + def __init__(self): + mobase.IPluginDiagnose.__init__(self) + mobase.IPluginGame.__init__(self) + + def activeProblems(self) -> list[int]: + return [5, 7] + + def shortDescription(self, key: int) -> str: + return f"short-{key}" + + def fullDescription(self, key: int) -> str: + return f"long-{key}" + + def hasGuidedFix(self, key: int) -> bool: + return key == 7 + + +def createPlugins() -> list[mobase.IPlugin]: + return [DummyDiagnose(), DummyDiagnoseAndGame()] # type: ignore diff --git a/libs/plugin_python/tests/runner/plugins/dummy-filemapper.py b/libs/plugin_python/tests/runner/plugins/dummy-filemapper.py new file mode 100644 index 0000000..1993b55 --- /dev/null +++ b/libs/plugin_python/tests/runner/plugins/dummy-filemapper.py @@ -0,0 +1,32 @@ +import mobase + + +class DummyFileMapper(mobase.IPluginFileMapper): + def mappings(self) -> list[mobase.Mapping]: + return [ + mobase.Mapping( + source="the source", destination="the destination", is_directory=False + ), + mobase.Mapping( + source="the other source", + destination="the other destination", + is_directory=True, + ), + ] + + +class DummyFileMapperAndGame(mobase.IPluginFileMapper, mobase.IPluginGame): + def __init__(self): + mobase.IPluginFileMapper.__init__(self) + mobase.IPluginGame.__init__(self) + + def mappings(self) -> list[mobase.Mapping]: + return [ + mobase.Mapping( + source="the source", destination="the destination", is_directory=False + ), + ] + + +def createPlugins() -> list[mobase.IPlugin]: + return [DummyFileMapper(), DummyFileMapperAndGame()] # type: ignore diff --git a/libs/plugin_python/tests/runner/plugins/dummy-game.py b/libs/plugin_python/tests/runner/plugins/dummy-game.py new file mode 100644 index 0000000..e17c9b8 --- /dev/null +++ b/libs/plugin_python/tests/runner/plugins/dummy-game.py @@ -0,0 +1,45 @@ +from collections.abc import Sequence + +from PyQt6.QtWidgets import QWidget + +import mobase + + +class DummyModDataChecker(mobase.ModDataChecker): + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + return mobase.ModDataChecker.VALID + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + return filetree + + +class DummySaveGameInfo(mobase.SaveGameInfo): + def getMissingAssets(self, save: mobase.ISaveGame) -> dict[str, Sequence[str]]: + return {} + + def getSaveGameWidget(self, parent: QWidget) -> mobase.ISaveGameInfoWidget | None: + return None + + +# we do not implement everything since this will do fine if we do not call anything +# from the C++ side +# +class DummyGame(mobase.IPluginGame): + _features: dict[type, object] + + def __init__(self): + super().__init__() + + self._features = { + mobase.ModDataChecker: DummyModDataChecker(), + mobase.SaveGameInfo: DummySaveGameInfo(), + } + + def _featureList(self) -> dict[type, object]: + return self._features + + +def createPlugin() -> mobase.IPlugin: + return DummyGame() # type: ignore diff --git a/libs/plugin_python/tests/runner/plugins/dummy-installer.py b/libs/plugin_python/tests/runner/plugins/dummy-installer.py new file mode 100644 index 0000000..3c4e67e --- /dev/null +++ b/libs/plugin_python/tests/runner/plugins/dummy-installer.py @@ -0,0 +1,46 @@ +# -*- encoding: utf-8 -*- + +from typing import Union, cast + +import mobase + + +# we do not implement everything since this will do fine if we do not call anything +# from the C++ side +# +class DummyInstaller(mobase.IPluginInstallerSimple): + def isManualInstaller(self) -> bool: + return False + + def priority(self) -> int: + return 10 + + def isArchiveSupported(self, tree: mobase.IFileTree) -> bool: + return tree.find("needed-file.txt") is not None + + def install( + self, + name: mobase.GuessedString, + tree: mobase.IFileTree, + version: str, + nexus_id: int, + ) -> Union[ + mobase.InstallResult, + mobase.IFileTree, + tuple[mobase.InstallResult, mobase.IFileTree, str, int], + ]: + if tree.find("needed-file.txt") is None: + return mobase.InstallResult.FAILED + + if tree.find("extra-file.txt"): + name.update("new name") + new_tree = tree.createOrphanTree() + new_tree.move(tree, "subtree") + cast(mobase.IFileTree, new_tree.find("subtree")).remove("extra-file.txt") + return new_tree + + return (mobase.InstallResult.NOT_ATTEMPTED, tree, "2.4.5", 33) + + +def createPlugin() -> mobase.IPlugin: + return DummyInstaller() # type: ignore diff --git a/libs/plugin_python/tests/runner/plugins/dummy-iplugin.py b/libs/plugin_python/tests/runner/plugins/dummy-iplugin.py new file mode 100644 index 0000000..eaaf6c0 --- /dev/null +++ b/libs/plugin_python/tests/runner/plugins/dummy-iplugin.py @@ -0,0 +1,29 @@ +import mobase + + +class DummyPlugin(mobase.IPlugin): + def init(self, organizer: mobase.IOrganizer) -> bool: + return True + + def author(self) -> str: + return "The Author" + + def name(self) -> str: + return "The Name" + + def description(self) -> str: + return "The Description" + + def version(self) -> mobase.VersionInfo: + return mobase.VersionInfo("1.3.0") + + def settings(self) -> list[mobase.PluginSetting]: + return [ + mobase.PluginSetting( + "a setting", "the setting description", default_value=12 + ) + ] + + +def createPlugin() -> mobase.IPlugin: + return DummyPlugin() diff --git a/libs/plugin_python/tests/runner/test_diagnose.cpp b/libs/plugin_python/tests/runner/test_diagnose.cpp new file mode 100644 index 0000000..467c9cb --- /dev/null +++ b/libs/plugin_python/tests/runner/test_diagnose.cpp @@ -0,0 +1,61 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "pythonrunner.h" + +#include + +#include +#include + +#include "MockOrganizer.h" + +using namespace MOBase; + +using ::testing::ElementsAre; + +TEST(IPluginDiagnose, Simple) +{ + const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + + auto runner = mo2::python::createPythonRunner(); + runner->initialize(); + + // load objects + const auto objects = runner->load(plugins_folder + "/dummy-diagnose.py"); + EXPECT_EQ(objects.size(), 3); + + // load the first IPluginDiagnose + { + IPluginDiagnose* plugin = qobject_cast(objects[0]); + EXPECT_NE(plugin, nullptr); + + ASSERT_THAT(plugin->activeProblems(), ElementsAre(1, 2)); + EXPECT_EQ(plugin->shortDescription(1), "short-1"); + EXPECT_EQ(plugin->fullDescription(1), "long-1"); + EXPECT_TRUE(plugin->hasGuidedFix(1)); + EXPECT_EQ(plugin->shortDescription(2), "short-2"); + EXPECT_EQ(plugin->fullDescription(2), "long-2"); + EXPECT_FALSE(plugin->hasGuidedFix(2)); + } + + // load the second one (this is cast before IPluginGame so should be before) + { + IPluginDiagnose* plugin = qobject_cast(objects[1]); + EXPECT_NE(plugin, nullptr); + + ASSERT_THAT(plugin->activeProblems(), ElementsAre(5, 7)); + EXPECT_EQ(plugin->shortDescription(5), "short-5"); + EXPECT_EQ(plugin->fullDescription(5), "long-5"); + EXPECT_FALSE(plugin->hasGuidedFix(5)); + EXPECT_EQ(plugin->shortDescription(7), "short-7"); + EXPECT_EQ(plugin->fullDescription(7), "long-7"); + EXPECT_TRUE(plugin->hasGuidedFix(7)); + } + + // load the game plugin + { + IPluginGame* plugin = qobject_cast(objects[2]); + EXPECT_NE(plugin, nullptr); + } +} diff --git a/libs/plugin_python/tests/runner/test_filemapper.cpp b/libs/plugin_python/tests/runner/test_filemapper.cpp new file mode 100644 index 0000000..5044283 --- /dev/null +++ b/libs/plugin_python/tests/runner/test_filemapper.cpp @@ -0,0 +1,64 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "pythonrunner.h" + +#include + +#include +#include + +#include "MockOrganizer.h" + +using namespace MOBase; + +TEST(IPluginFileMapper, Simple) +{ + const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + + auto runner = mo2::python::createPythonRunner(); + runner->initialize(); + + // load objects + const auto objects = runner->load(plugins_folder + "/dummy-filemapper.py"); + EXPECT_EQ(objects.size(), 3); + + // load the first IPluginFileMapper + { + IPluginFileMapper* plugin = qobject_cast(objects[0]); + EXPECT_NE(plugin, nullptr); + + const auto m = plugin->mappings(); + EXPECT_EQ(m.size(), 2); + + EXPECT_EQ(m[0].source, "the source"); + EXPECT_EQ(m[0].destination, "the destination"); + EXPECT_EQ(m[0].isDirectory, false); + EXPECT_EQ(m[0].createTarget, false); + + EXPECT_EQ(m[1].source, "the other source"); + EXPECT_EQ(m[1].destination, "the other destination"); + EXPECT_EQ(m[1].isDirectory, true); + EXPECT_EQ(m[1].createTarget, false); + } + + // load the second one (this is cast before IPluginGame so should be before) + { + IPluginFileMapper* plugin = qobject_cast(objects[1]); + EXPECT_NE(plugin, nullptr); + + const auto m = plugin->mappings(); + EXPECT_EQ(m.size(), 1); + + EXPECT_EQ(m[0].source, "the source"); + EXPECT_EQ(m[0].destination, "the destination"); + EXPECT_EQ(m[0].isDirectory, false); + EXPECT_EQ(m[0].createTarget, false); + } + + // load the game plugin + { + IPluginGame* plugin = qobject_cast(objects[2]); + EXPECT_NE(plugin, nullptr); + } +} diff --git a/libs/plugin_python/tests/runner/test_game.cpp b/libs/plugin_python/tests/runner/test_game.cpp new file mode 100644 index 0000000..0d1d718 --- /dev/null +++ b/libs/plugin_python/tests/runner/test_game.cpp @@ -0,0 +1,28 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "pythonrunner.h" + +#include + +#include + +#include "MockOrganizer.h" + +using namespace MOBase; + +TEST(IPluginGame, Simple) +{ + const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + + auto runner = mo2::python::createPythonRunner(); + runner->initialize(); + + // load objects + const auto objects = runner->load(plugins_folder + "/dummy-game.py"); + EXPECT_EQ(objects.size(), 1); + + // load the IPlugin + IPluginGame* plugin = qobject_cast(objects[0]); + EXPECT_NE(plugin, nullptr); +} diff --git a/libs/plugin_python/tests/runner/test_installer.cpp b/libs/plugin_python/tests/runner/test_installer.cpp new file mode 100644 index 0000000..349c904 --- /dev/null +++ b/libs/plugin_python/tests/runner/test_installer.cpp @@ -0,0 +1,112 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include + +#include "MockOrganizer.h" +#include "pythonrunner.h" + +#include + +#include "DummyFileTree.h" + +using namespace MOBase; + +TEST(IPluginInstaller, Simple) +{ + const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + + auto runner = mo2::python::createPythonRunner(); + runner->initialize(); + + // load objects + const auto objects = runner->load(plugins_folder + "/dummy-installer.py"); + EXPECT_EQ(objects.size(), 1); + + // load the IPlugin + IPluginInstallerSimple* plugin = qobject_cast(objects[0]); + EXPECT_NE(plugin, nullptr); + + // basic tests + EXPECT_EQ(plugin->priority(), 10); + EXPECT_EQ(plugin->isManualInstaller(), false); + + GuessedValue name{"default name"}; + QString version = "1.0.0"; + int nexus_id = 12; + + // invalid tree + { + std::shared_ptr invalid_tree = + std::make_shared(nullptr, "root"); + invalid_tree->addFile("some file"); + invalid_tree->addDirectory("some directory")->addFile("some other file"); + EXPECT_EQ(plugin->isArchiveSupported(invalid_tree), false); + EXPECT_EQ(plugin->install(name, invalid_tree, version, nexus_id), + IPluginInstaller::RESULT_FAILED); + + // extra arguments stay the same + EXPECT_EQ(name, "default name"); + EXPECT_EQ(version, "1.0.0"); + EXPECT_EQ(nexus_id, 12); + } + + // valid simple tree + { + std::shared_ptr valid_tree = + std::make_shared(nullptr, "root"); + const auto initial_tree = valid_tree; + valid_tree->addFile("needed-file.txt"); + valid_tree->addFile("some file"); + valid_tree->addDirectory("some directory")->addFile("some other file"); + + // valid tree + EXPECT_EQ(plugin->isArchiveSupported(valid_tree), true); + EXPECT_EQ(plugin->install(name, valid_tree, version, nexus_id), + IPluginInstaller::RESULT_NOTATTEMPTED); + + // name is not modified + EXPECT_EQ(name, "default name"); + + // tree is not modified + EXPECT_EQ(valid_tree, initial_tree); + + // version and ID are modified + EXPECT_EQ(version, "2.4.5"); + EXPECT_EQ(nexus_id, 33); + } + + // "complex" tree + { + std::shared_ptr complex_tree = + std::make_shared(nullptr, "root"); + const auto initial_tree = complex_tree; + const auto needed_file = complex_tree->addFile("needed-file.txt"); + const auto extra_file = complex_tree->addFile("extra-file.txt"); + const auto some_file = complex_tree->addFile("some file"); + const auto some_directory = complex_tree->addDirectory("some directory"); + some_directory->addFile("some other file"); + + // valid tree + EXPECT_EQ(plugin->isArchiveSupported(complex_tree), true); + EXPECT_EQ(plugin->install(name, complex_tree, version, nexus_id), + IPluginInstaller::RESULT_SUCCESS); + + // name is modified + EXPECT_EQ(name, "new name"); + + // tree is modified + EXPECT_EQ(complex_tree->findDirectory("subtree")->find("needed-file.txt"), + needed_file); + EXPECT_EQ( + complex_tree->findDirectory("subtree")->findDirectory("extra-file.txt"), + nullptr); + EXPECT_EQ( + complex_tree->findDirectory("subtree")->findDirectory("some directory"), + some_directory); + + // version and ID are not modified (from previous call) + EXPECT_EQ(version, "2.4.5"); + EXPECT_EQ(nexus_id, 33); + } +} diff --git a/libs/plugin_python/tests/runner/test_iplugin.cpp b/libs/plugin_python/tests/runner/test_iplugin.cpp new file mode 100644 index 0000000..27c4829 --- /dev/null +++ b/libs/plugin_python/tests/runner/test_iplugin.cpp @@ -0,0 +1,43 @@ +#include +#include + +#include + +#include "MockOrganizer.h" +#include "pythonrunner.h" + +#include + +using namespace MOBase; + +TEST(IPlugin, Basic) +{ + const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + + auto runner = mo2::python::createPythonRunner(); + runner->initialize(); + + // load objects + const auto objects = runner->load(plugins_folder + "/dummy-iplugin.py"); + EXPECT_EQ(objects.size(), 1); + + // load the IPlugin + const IPlugin* plugin = qobject_cast(objects[0]); + EXPECT_NE(plugin, nullptr); + + EXPECT_EQ(plugin->author(), "The Author"); + EXPECT_EQ(plugin->name(), "The Name"); + EXPECT_EQ(plugin->version(), VersionInfo(1, 3, 0)); + EXPECT_EQ(plugin->description(), "The Description"); + + // settings + const auto settings = plugin->settings(); + EXPECT_EQ(settings.size(), 1); + EXPECT_EQ(settings[0].key, "a setting"); + EXPECT_EQ(settings[0].description, "the setting description"); + EXPECT_EQ(settings[0].defaultValue.userType(), QMetaType::Type::Int); + EXPECT_EQ(settings[0].defaultValue.toInt(), 12); + + // no translation, no custom implementation -> name() + EXPECT_EQ(plugin->localizedName(), "The Name"); +} diff --git a/libs/plugin_python/tests/runner/test_lifetime.cpp b/libs/plugin_python/tests/runner/test_lifetime.cpp new file mode 100644 index 0000000..bf057d3 --- /dev/null +++ b/libs/plugin_python/tests/runner/test_lifetime.cpp @@ -0,0 +1,47 @@ +#include "gtest/gtest.h" + +#include "MockOrganizer.h" +#include "pythonrunner.h" + +#include + +TEST(Lifetime, Plugins) +{ + const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + + auto runner = mo2::python::createPythonRunner(); + runner->initialize(); + + { + const auto objects = runner->load(plugins_folder + "/dummy-iplugin.py"); + + // we found one plugin + EXPECT_EQ(objects.size(), 1); + + // check that deleting the object actually destroys it + bool destroyed = false; + QObject::connect(objects[0], &QObject::destroyed, [&destroyed]() { + destroyed = true; + }); + delete objects[0]; + EXPECT_EQ(destroyed, true); + } + + // same things but with a parent + { + QObject* dummy_parent = new QObject(); + const auto objects = runner->load(plugins_folder + "/dummy-iplugin.py"); + + // we found one plugin + EXPECT_EQ(objects.size(), 1); + objects[0]->setParent(dummy_parent); + + // check that deleting the object actually destroys it + bool destroyed = false; + QObject::connect(objects[0], &QObject::destroyed, [&destroyed]() { + destroyed = true; + }); + delete dummy_parent; + EXPECT_EQ(destroyed, true); + } +} diff --git a/libs/plugin_python/typings/generate.py b/libs/plugin_python/typings/generate.py new file mode 100644 index 0000000..1983186 --- /dev/null +++ b/libs/plugin_python/typings/generate.py @@ -0,0 +1,38 @@ +import os +import site +import sys +from pathlib import Path +from typing import cast + +import pybind11_stubgen as py11stubs + +typings_dir = Path(__file__).parent +mobase_tests_dir = Path(__file__).parent.parent.joinpath( + "vsbuild", "tests", "python", "pylibs", "mobase_tests" +) + +site.addsitedir(str(mobase_tests_dir.parent)) + +os.add_dll_directory(str(Path(cast(str, os.getenv("QT_ROOT"))).joinpath("bin"))) +os.add_dll_directory(str(os.getenv("UIBASE_PATH"))) + +from PyQt6.QtWidgets import QApplication # noqa: E402 + +app = QApplication(sys.argv) + +args = py11stubs.arg_parser().parse_args(["dummy"], namespace=py11stubs.CLIArgs()) + +parser = py11stubs.stub_parser_from_args(args) +printer = py11stubs.Printer(invalid_expr_as_ellipses=True) # type: ignore + +for path in mobase_tests_dir.glob("*.pyd"): + name = path.name.split(".")[0] + py11stubs.run( + parser, + printer, + f"mobase_tests.{name}", + typings_dir.joinpath("mobase_tests"), + sub_dir=None, + dry_run=False, + writer=py11stubs.Writer(stub_ext="pyi"), # type: ignore + ) diff --git a/libs/plugin_python/typings/mobase_tests/argument_wrapper.pyi b/libs/plugin_python/typings/mobase_tests/argument_wrapper.pyi new file mode 100644 index 0000000..a96e49a --- /dev/null +++ b/libs/plugin_python/typings/mobase_tests/argument_wrapper.pyi @@ -0,0 +1,27 @@ +from __future__ import annotations + +__all__ = [ + "fn1_raw", + "fn1_wrap", + "fn1_wrap_0", + "fn2_raw", + "fn2_wrap", + "fn2_wrap_0", + "fn3_raw", + "fn3_wrap", + "fn3_wrap_0", + "fn3_wrap_0_2", + "fn3_wrap_2", +] + +def fn1_raw(arg0: str) -> str: ... +def fn1_wrap(arg0: int | str) -> str: ... +def fn1_wrap_0(arg0: int | str) -> str: ... +def fn2_raw(arg0: int) -> int: ... +def fn2_wrap(arg0: int | str) -> int: ... +def fn2_wrap_0(arg0: int | str) -> int: ... +def fn3_raw(arg0: int, arg1: list[int], arg2: str) -> str: ... +def fn3_wrap(arg0: int | str, arg1: list[int], arg2: int | str) -> str: ... +def fn3_wrap_0(arg0: int | str, arg1: list[int], arg2: str) -> str: ... +def fn3_wrap_0_2(arg0: int | str, arg1: list[int], arg2: int | str) -> str: ... +def fn3_wrap_2(arg0: int, arg1: list[int], arg2: int | str) -> str: ... diff --git a/libs/plugin_python/typings/mobase_tests/filetree.pyi b/libs/plugin_python/typings/mobase_tests/filetree.pyi new file mode 100644 index 0000000..93eab63 --- /dev/null +++ b/libs/plugin_python/typings/mobase_tests/filetree.pyi @@ -0,0 +1,9 @@ +from __future__ import annotations + +import mobase + +__all__ = ["is_directory", "is_file", "value"] + +def is_directory(arg0: mobase.IFileTree.FileTypes) -> bool: ... +def is_file(arg0: mobase.IFileTree.FileTypes) -> bool: ... +def value(arg0: mobase.IFileTree.FileTypes) -> int: ... diff --git a/libs/plugin_python/typings/mobase_tests/functional.pyi b/libs/plugin_python/typings/mobase_tests/functional.pyi new file mode 100644 index 0000000..7e58f0e --- /dev/null +++ b/libs/plugin_python/typings/mobase_tests/functional.pyi @@ -0,0 +1,19 @@ +from __future__ import annotations + +import typing + +__all__ = ["fn_0_arg", "fn_0_or_1_arg", "fn_1_arg", "fn_1_or_2_or_3_arg", "fn_2_arg"] + +def fn_0_arg(arg0: typing.Callable[[], int]) -> int: ... +@typing.overload +def fn_0_or_1_arg(arg0: typing.Callable[[], int]) -> int: ... +@typing.overload +def fn_0_or_1_arg(arg0: typing.Callable[[int], int]) -> int: ... +def fn_1_arg(arg0: typing.Callable[[int], int], arg1: int) -> int: ... +@typing.overload +def fn_1_or_2_or_3_arg(arg0: typing.Callable[[int], int]) -> int: ... +@typing.overload +def fn_1_or_2_or_3_arg(arg0: typing.Callable[[int, int], int]) -> int: ... +@typing.overload +def fn_1_or_2_or_3_arg(arg0: typing.Callable[[int, int, int], int]) -> int: ... +def fn_2_arg(arg0: typing.Callable[[int, int], int], arg1: int, arg2: int) -> int: ... diff --git a/libs/plugin_python/typings/mobase_tests/guessed_string.pyi b/libs/plugin_python/typings/mobase_tests/guessed_string.pyi new file mode 100644 index 0000000..a03d9b6 --- /dev/null +++ b/libs/plugin_python/typings/mobase_tests/guessed_string.pyi @@ -0,0 +1,16 @@ +from __future__ import annotations + +import typing + +import mobase + +__all__ = ["get_from_callback", "get_value", "get_variants", "set_from_callback"] + +def get_from_callback( + arg0: typing.Callable[[mobase.GuessedString], None], +) -> str: ... +def get_value(arg0: str | mobase.GuessedString) -> str: ... +def get_variants(arg0: str | mobase.GuessedString) -> set[str]: ... +def set_from_callback( + arg0: mobase.GuessedString, arg1: typing.Callable[[mobase.GuessedString], None] +) -> None: ... diff --git a/libs/plugin_python/typings/mobase_tests/organizer.pyi b/libs/plugin_python/typings/mobase_tests/organizer.pyi new file mode 100644 index 0000000..dd5136a --- /dev/null +++ b/libs/plugin_python/typings/mobase_tests/organizer.pyi @@ -0,0 +1,7 @@ +from __future__ import annotations + +import mobase + +__all__ = ["organizer"] + +def organizer() -> mobase.IOrganizer: ... diff --git a/libs/plugin_python/typings/mobase_tests/qt.pyi b/libs/plugin_python/typings/mobase_tests/qt.pyi new file mode 100644 index 0000000..088ef2a --- /dev/null +++ b/libs/plugin_python/typings/mobase_tests/qt.pyi @@ -0,0 +1,109 @@ +from __future__ import annotations + +import typing + +import PyQt6.QtCore + +from mobase import MoVariant + +__all__ = [ + "SimpleEnum", + "consume_qstring_with_emoji", + "create_qstring_with_emoji", + "datetime_from_string", + "datetime_to_string", + "int_to_qstring", + "qflags_create", + "qflags_explode", + "qmap_to_length", + "qstring_to_int", + "qstring_to_stdstring", + "qstringlist_at", + "qstringlist_join", + "qvariant_bool", + "qvariant_from_bool", + "qvariant_from_int", + "qvariant_from_list", + "qvariant_from_map", + "qvariant_from_none", + "qvariant_from_str", + "qvariant_int", + "qvariant_list", + "qvariant_map", + "qvariant_none", + "qvariant_str", + "stdstring_to_qstring", +] + +class SimpleEnum: + """ + Members: + + Value0 + + Value1 + + Value2 + """ + + Value0: typing.ClassVar[SimpleEnum] # value = + Value1: typing.ClassVar[SimpleEnum] # value = + Value2: typing.ClassVar[SimpleEnum] # value = + __members__: typing.ClassVar[ + dict[str, SimpleEnum] + ] # value = {'Value0': , 'Value1': , 'Value2': } + def __and__(self, other: typing.Any) -> typing.Any: ... + def __eq__(self, other: typing.Any) -> bool: ... + def __ge__(self, other: typing.Any) -> bool: ... + def __getstate__(self) -> int: ... + def __gt__(self, other: typing.Any) -> bool: ... + def __hash__(self) -> int: ... + def __index__(self) -> int: ... + def __init__(self, value: int) -> None: ... + def __int__(self) -> int: ... + def __invert__(self) -> typing.Any: ... + def __le__(self, other: typing.Any) -> bool: ... + def __lt__(self, other: typing.Any) -> bool: ... + def __ne__(self, other: typing.Any) -> bool: ... + def __or__(self, other: typing.Any) -> typing.Any: ... + def __rand__(self, other: typing.Any) -> typing.Any: ... + def __repr__(self) -> str: ... + def __ror__(self, other: typing.Any) -> typing.Any: ... + def __rxor__(self, other: typing.Any) -> typing.Any: ... + def __setstate__(self, state: int) -> None: ... + def __str__(self) -> str: ... + def __xor__(self, other: typing.Any) -> typing.Any: ... + @property + def name(self) -> str: ... + @property + def value(self) -> int: ... + +def consume_qstring_with_emoji(arg0: str) -> int: ... +def create_qstring_with_emoji() -> str: ... +def datetime_from_string( + string: str, format: PyQt6.QtCore.Qt.DateFormat = ... +) -> PyQt6.QtCore.QDateTime: ... +def datetime_to_string( + datetime: PyQt6.QtCore.QDateTime, format: PyQt6.QtCore.Qt.DateFormat = ... +) -> str: ... +def int_to_qstring(arg0: int) -> str: ... +def qflags_create(arg0: bool, arg1: bool, arg2: bool) -> int: ... +def qflags_explode(arg0: int) -> tuple[int, bool, bool, bool]: ... +def qmap_to_length(arg0: dict[str, str]) -> dict[str, int]: ... +def qstring_to_int(arg0: str) -> int: ... +def qstring_to_stdstring(arg0: str) -> str: ... +def qstringlist_at(arg0: typing.Sequence[str], arg1: int) -> str: ... +def qstringlist_join(arg0: typing.Sequence[str], arg1: str) -> str: ... +def qvariant_bool() -> MoVariant: ... +def qvariant_from_bool(arg0: MoVariant) -> tuple[bool, bool]: ... +def qvariant_from_int(arg0: MoVariant) -> tuple[bool, int]: ... +def qvariant_from_list(arg0: MoVariant) -> tuple[bool, typing.Sequence[MoVariant]]: ... +def qvariant_from_map(arg0: MoVariant) -> tuple[bool, dict[str, MoVariant]]: ... +def qvariant_from_none(arg0: MoVariant) -> tuple[bool, bool]: ... +def qvariant_from_str(arg0: MoVariant) -> tuple[bool, str]: ... +def qvariant_int() -> MoVariant: ... +def qvariant_list() -> MoVariant: ... +def qvariant_map() -> dict[str, MoVariant]: ... +def qvariant_none() -> MoVariant: ... +def qvariant_str() -> MoVariant: ... +def stdstring_to_qstring(arg0: str) -> str: ... diff --git a/libs/plugin_python/typings/mobase_tests/qt_widgets.pyi b/libs/plugin_python/typings/mobase_tests/qt_widgets.pyi new file mode 100644 index 0000000..dfda4f9 --- /dev/null +++ b/libs/plugin_python/typings/mobase_tests/qt_widgets.pyi @@ -0,0 +1,34 @@ +from __future__ import annotations + +import typing + +import PyQt6.QtWidgets + +__all__ = [ + "CustomWidget", + "get", + "heightForWidth", + "is_alive", + "is_owned_cpp", + "make_widget_own_cpp", + "make_widget_own_py", + "send_to_cpp", + "set_parent", +] + +class CustomWidget: + def __getattr__(self, arg0: str) -> typing.Any: ... + def __init__( + self, name: str, parent: PyQt6.QtWidgets.QWidget | None = None + ) -> None: ... + def _widget(self) -> PyQt6.QtWidgets.QWidget: ... + def set_parent_cpp(self) -> None: ... + +def get(arg0: str) -> PyQt6.QtWidgets.QWidget: ... +def heightForWidth(arg0: str, arg1: int) -> int: ... +def is_alive(arg0: str) -> bool: ... +def is_owned_cpp(arg0: str) -> bool: ... +def make_widget_own_cpp(arg0: str) -> PyQt6.QtWidgets.QWidget: ... +def make_widget_own_py(arg0: str) -> PyQt6.QtWidgets.QWidget: ... +def send_to_cpp(arg0: str, arg1: PyQt6.QtWidgets.QWidget) -> None: ... +def set_parent(arg0: PyQt6.QtWidgets.QWidget) -> None: ... diff --git a/libs/plugin_python/typings/mobase_tests/shared_cpp_owner.pyi b/libs/plugin_python/typings/mobase_tests/shared_cpp_owner.pyi new file mode 100644 index 0000000..789632e --- /dev/null +++ b/libs/plugin_python/typings/mobase_tests/shared_cpp_owner.pyi @@ -0,0 +1,22 @@ +from __future__ import annotations + +__all__ = [ + "Base", + "call_fn", + "clear", + "create", + "create_and_store", + "is_alive", + "store", +] + +class Base: + def __init__(self, arg0: str) -> None: ... + def fn(self) -> str: ... + +def call_fn(arg0: str) -> str: ... +def clear() -> None: ... +def create(arg0: str) -> Base: ... +def create_and_store(arg0: str) -> Base: ... +def is_alive(arg0: str) -> bool: ... +def store(arg0: Base) -> None: ... diff --git a/libs/plugin_python/vcpkg.json b/libs/plugin_python/vcpkg.json new file mode 100644 index 0000000..db9eb08 --- /dev/null +++ b/libs/plugin_python/vcpkg.json @@ -0,0 +1,34 @@ +{ + "dependencies": ["pybind11"], + "features": { + "testing": { + "description": "Build Plugin Python tests.", + "dependencies": ["gtest"] + }, + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43", + "packages": ["boost*", "boost-*"] + }, + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "228cda39fe9d1eeed789c0ef64fd1235dab3b11e", + "packages": ["mo2-*", "pybind11", "spdlog"] + } + ] + } +} diff --git a/libs/preview_base/.clang-format b/libs/preview_base/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/preview_base/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/preview_base/.git-blame-ignore-revs b/libs/preview_base/.git-blame-ignore-revs new file mode 100644 index 0000000..9079b32 --- /dev/null +++ b/libs/preview_base/.git-blame-ignore-revs @@ -0,0 +1 @@ +01daa7e9c9f396f83451703fc6cdc046a4e87b16 diff --git a/libs/preview_base/.gitattributes b/libs/preview_base/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/preview_base/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/preview_base/.github/workflows/build.yml b/libs/preview_base/.github/workflows/build.yml new file mode 100644 index 0000000..f210fc0 --- /dev/null +++ b/libs/preview_base/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build Preview Base + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Preview Base + id: build-preview-base + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase diff --git a/libs/preview_base/.github/workflows/linting.yml b/libs/preview_base/.github/workflows/linting.yml new file mode 100644 index 0000000..9a7ba16 --- /dev/null +++ b/libs/preview_base/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Preview Base Plugin + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/preview_base/.gitignore b/libs/preview_base/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/preview_base/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/preview_base/.pre-commit-config.yaml b/libs/preview_base/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/preview_base/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/preview_base/CMakeLists.txt b/libs/preview_base/CMakeLists.txt new file mode 100644 index 0000000..e8897ac --- /dev/null +++ b/libs/preview_base/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(preview_base) + +add_subdirectory(src) diff --git a/libs/preview_base/CMakePresets.json b/libs/preview_base/CMakePresets.json new file mode 100644 index 0000000..5a63c77 --- /dev/null +++ b/libs/preview_base/CMakePresets.json @@ -0,0 +1,67 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/preview_base/src/CMakeLists.txt b/libs/preview_base/src/CMakeLists.txt new file mode 100644 index 0000000..b59dd77 --- /dev/null +++ b/libs/preview_base/src/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.16) + + +add_library(preview_base SHARED previewbase.cpp previewbase.h) +mo2_configure_plugin(preview_base NO_SOURCES WARNINGS 4) +target_link_libraries(preview_base PRIVATE mo2::uibase) +mo2_install_plugin(preview_base) diff --git a/libs/preview_base/src/preview_base_en.ts b/libs/preview_base/src/preview_base_en.ts new file mode 100644 index 0000000..3c66964 --- /dev/null +++ b/libs/preview_base/src/preview_base_en.ts @@ -0,0 +1,27 @@ + + + + + PreviewBase + + + Preview Base + + + + + Supports previewing various types of data files + + + + + Enable previewing of basic file types, such as images and text files. + + + + + Specify a list of comma separated extensions (without ".") that should not be previewed by this plugin. + + + + diff --git a/libs/preview_base/src/previewbase.cpp b/libs/preview_base/src/previewbase.cpp new file mode 100644 index 0000000..cd393d8 --- /dev/null +++ b/libs/preview_base/src/previewbase.cpp @@ -0,0 +1,185 @@ +/* +Copyright (C) 2014 Sebastian Herbord. All rights reserved. + +This file is part of Base Preview plugin for MO + +Base Preview plugin is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Base Preview plugin is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Base Preview plugin. If not, see . +*/ + +#include "previewbase.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace MOBase; + +PreviewBase::PreviewBase() : m_MOInfo(nullptr) {} + +bool PreviewBase::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + + const QStringList& blacklist = + m_MOInfo->pluginSetting(name(), "blacklisted_extensions") + .toString() + .toLower() + .split(','); + + // set up image reader to be used for all image types qt (the current installation) + // supports + auto imageReader = + std::bind(&PreviewBase::genImagePreview, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3); + + foreach (const QByteArray& fileType, QImageReader::supportedImageFormats()) { + auto strFileType = QString(fileType).toLower(); + + // skip dds as that one is handled by the dds preview plugin. + if (strFileType == "dds" || blacklist.contains(strFileType)) + continue; + + m_PreviewGenerators[strFileType] = imageReader; + } + + const QStringList supportedTextFormats = {"txt", "ini", "json", "log", "cfg", "psc"}; + + auto textReader = std::bind(&PreviewBase::genTxtPreview, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3); + + foreach (const QString fileType, supportedTextFormats) { + + // skip blacklisted ones + if (blacklist.contains(fileType)) + continue; + + m_PreviewGenerators[fileType] = textReader; + } + + return true; +} + +QString PreviewBase::name() const +{ + return "Preview Base"; +} + +QString PreviewBase::localizedName() const +{ + return tr("Preview Base"); +} + +QString PreviewBase::author() const +{ + return "Tannin"; +} + +QString PreviewBase::description() const +{ + return tr("Supports previewing various types of data files"); +} + +MOBase::VersionInfo PreviewBase::version() const +{ + return VersionInfo(1, 1, 0, VersionInfo::RELEASE_FINAL); +} + +QList PreviewBase::settings() const +{ + QList result; + result.push_back(PluginSetting( + "enabled", + tr("Enable previewing of basic file types, such as images and text files."), + QVariant(true))); + result.push_back( + PluginSetting("blacklisted_extensions", + tr("Specify a list of comma separated extensions (without \".\") " + "that should not be previewed by this plugin."), + QVariant(""))); + return result; +} + +std::set PreviewBase::supportedExtensions() const +{ + std::set extensions; + for (const auto& generator : m_PreviewGenerators) { + extensions.insert(generator.first); + } + + return extensions; +} + +QWidget* PreviewBase::genFilePreview(const QString& fileName, + const QSize& maxSize) const +{ + return genDataPreview(nullptr, fileName, maxSize); +} + +QWidget* PreviewBase::genDataPreview(const QByteArray& fileData, + const QString& fileName, + const QSize& maxSize) const +{ + auto iter = m_PreviewGenerators.find(QFileInfo(fileName).suffix().toLower()); + if (iter != m_PreviewGenerators.end()) { + return iter->second(fileName, maxSize, fileData); + } else { + return nullptr; + } +} + +QWidget* PreviewBase::genImagePreview(const QString& fileName, const QSize&, + const QByteArray& fileData) const +{ + QLabel* label = new QLabel(); + QPixmap pic; + if (fileData == nullptr) { + pic = QPixmap(fileName); + } else { + pic.loadFromData(fileData); + } + QSize screenSize = QApplication::primaryScreen()->geometry().size(); + // ensure the output image is no more than 80% of the screen height. + // If the aspect ratio is higher than that of the screen this would still allow the + // image to extend beyond the screen but it ensures you can drag the window and close + // it + int maxHeight = static_cast(screenSize.height() * 0.8f); + if (pic.size().height() > maxHeight) { + pic = pic.scaledToHeight(maxHeight, Qt::SmoothTransformation); + } + label->setPixmap(pic); + return label; +} + +QWidget* PreviewBase::genTxtPreview(const QString& fileName, const QSize&, + const QByteArray& fileData) const +{ + QTextEdit* edit = new QTextEdit(); + if (fileData == nullptr) { + edit->setText(MOBase::readFileText(fileName)); + } else { + edit->setText(MOBase::decodeTextData(fileData)); + } + edit->setReadOnly(true); + return edit; +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(previewBase, PreviewBase) +#endif diff --git a/libs/preview_base/src/previewbase.h b/libs/preview_base/src/previewbase.h new file mode 100644 index 0000000..d4ffc14 --- /dev/null +++ b/libs/preview_base/src/previewbase.h @@ -0,0 +1,68 @@ +/* +Copyright (C) 2014 Sebastian Herbord. All rights reserved. + +This file is part of Base Preview plugin for MO + +Base Preview plugin is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Base Preview plugin is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Base Preview plugin. If not, see . +*/ + +#ifndef PREVIEWBASE_H +#define PREVIEWBASE_H + +#include + +#include + +class PreviewBase : public MOBase::IPluginPreview +{ + + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginPreview) + Q_PLUGIN_METADATA(IID "org.tannin.PreviewBase") + +public: + PreviewBase(); + +public: + virtual bool init(MOBase::IOrganizer* moInfo) override; + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + +public: + virtual std::set supportedExtensions() const; + virtual bool supportsArchives() const override { return true; } + virtual QWidget* genFilePreview(const QString& fileName, const QSize& maxSize) const; + virtual QWidget* genDataPreview(const QByteArray& fileData, const QString& fileName, + const QSize& maxSize) const override; + +private: + QWidget* genImagePreview(const QString& fileName, const QSize& maxSize, + const QByteArray& fileData) const; + QWidget* genTxtPreview(const QString& fileName, const QSize& maxSize, + const QByteArray& fileData) const; + +private: + std::map> + m_PreviewGenerators; + +private: + const MOBase::IOrganizer* m_MOInfo; +}; + +#endif // PREVIEWBASE_H diff --git a/libs/preview_base/vcpkg.json b/libs/preview_base/vcpkg.json new file mode 100644 index 0000000..0740fee --- /dev/null +++ b/libs/preview_base/vcpkg.json @@ -0,0 +1,23 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673", + "packages": ["mo2-*"] + } + ] + } +} diff --git a/libs/preview_bsa/.clang-format b/libs/preview_bsa/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/preview_bsa/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/preview_bsa/.gitattributes b/libs/preview_bsa/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/preview_bsa/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/preview_bsa/.github/workflows/build.yml b/libs/preview_bsa/.github/workflows/build.yml new file mode 100644 index 0000000..030a354 --- /dev/null +++ b/libs/preview_bsa/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build Preview BSA + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Build Preview BSA + id: build-preview-bsa + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase bsatk diff --git a/libs/preview_bsa/.github/workflows/linting.yml b/libs/preview_bsa/.github/workflows/linting.yml new file mode 100644 index 0000000..8de4aa2 --- /dev/null +++ b/libs/preview_bsa/.github/workflows/linting.yml @@ -0,0 +1,16 @@ +name: Lint Preview BSA Plugin + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." diff --git a/libs/preview_bsa/.gitignore b/libs/preview_bsa/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/preview_bsa/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/preview_bsa/.pre-commit-config.yaml b/libs/preview_bsa/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/preview_bsa/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/preview_bsa/CMakeLists.txt b/libs/preview_bsa/CMakeLists.txt new file mode 100644 index 0000000..e7757aa --- /dev/null +++ b/libs/preview_bsa/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.16) + +project(preview_bsa) +add_subdirectory(src) diff --git a/libs/preview_bsa/CMakePresets.json b/libs/preview_bsa/CMakePresets.json new file mode 100644 index 0000000..5a63c77 --- /dev/null +++ b/libs/preview_bsa/CMakePresets.json @@ -0,0 +1,67 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/preview_bsa/src/CMakeLists.txt b/libs/preview_bsa/src/CMakeLists.txt new file mode 100644 index 0000000..fe85679 --- /dev/null +++ b/libs/preview_bsa/src/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB preview_bsa_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(preview_bsa SHARED ${preview_bsa_SOURCES}) +mo2_configure_plugin(preview_bsa NO_SOURCES WARNINGS 4) +target_link_libraries(preview_bsa PRIVATE mo2::uibase mo2::bsatk) +mo2_install_plugin(preview_bsa) diff --git a/libs/preview_bsa/src/preview_bsa_en.ts b/libs/preview_bsa/src/preview_bsa_en.ts new file mode 100644 index 0000000..0a836bc --- /dev/null +++ b/libs/preview_bsa/src/preview_bsa_en.ts @@ -0,0 +1,45 @@ + + + + + PreviewBsa + + + Preview BSA + + + + + Supports previewing contents of Bethesda Archive files, BSAs and BA2s + + + + + Enable previewing of BSA file contents + + + + + Archive Format: %1 , Compression: %2 , File count: %3 , Version: %4 , Archive type: %5 , Archive flags: %6 + + + + + yes + + + + + no + + + + + SimpleFileTreeModel + + + File Name + + + + diff --git a/libs/preview_bsa/src/previewbsa.cpp b/libs/preview_bsa/src/previewbsa.cpp new file mode 100644 index 0000000..e21df2f --- /dev/null +++ b/libs/preview_bsa/src/previewbsa.cpp @@ -0,0 +1,234 @@ +/* +Copyright (C) 2014 Sebastian Herbord. All rights reserved. + +This file is part of Bsa Preview plugin for MO + +Bsa Preview plugin is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Bsa Preview plugin is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Bsa Preview plugin. If not, see . +*/ +#include "previewbsa.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "simplefiletreemodel.h" + +using namespace MOBase; + +PreviewBsa::PreviewBsa() : m_MOInfo(nullptr) {} + +bool PreviewBsa::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + + auto bsaPreview = std::bind(&PreviewBsa::genBsaPreview, this, std::placeholders::_1, + std::placeholders::_2); + + m_PreviewGenerators["bsa"] = bsaPreview; + m_PreviewGenerators["ba2"] = bsaPreview; + + return true; +} + +QString PreviewBsa::name() const +{ + return "Preview Bsa"; +} + +QString PreviewBsa::localizedName() const +{ + return tr("Preview BSA"); +} + +QString PreviewBsa::author() const +{ + return "AL12 & MO2 Team"; +} + +QString PreviewBsa::description() const +{ + return tr("Supports previewing contents of Bethesda Archive files, BSAs and BA2s"); +} + +MOBase::VersionInfo PreviewBsa::version() const +{ + return VersionInfo(1, 1, 0, VersionInfo::RELEASE_FINAL); +} + +QList PreviewBsa::settings() const +{ + QList result; + result.push_back(PluginSetting( + "enabled", tr("Enable previewing of BSA file contents"), QVariant(true))); + return result; +} + +std::set PreviewBsa::supportedExtensions() const +{ + std::set extensions; + for (const auto& generator : m_PreviewGenerators) { + extensions.insert(generator.first); + } + + return extensions; +} + +QWidget* PreviewBsa::genFilePreview(const QString& fileName, const QSize& maxSize) const +{ + auto iter = m_PreviewGenerators.find(QFileInfo(fileName).suffix().toLower()); + if (iter != m_PreviewGenerators.end()) { + return iter->second(fileName, maxSize); + } else { + return nullptr; + } +} + +void PreviewBsa::readFiles(const BSA::Folder::Ptr archiveFolder) +{ + const auto fileCount = archiveFolder->getNumFiles(); + for (unsigned int i = 0; i < fileCount; ++i) { + const BSA::File::Ptr file = archiveFolder->getFile(i); + + m_Files << QString::fromStdString(file->getFilePath()); + } + + // recurse into subdirectories + const auto dirCount = archiveFolder->getNumSubFolders(); + for (unsigned int i = 0; i < dirCount; ++i) { + const BSA::Folder::Ptr folder = archiveFolder->getSubFolder(i); + + readFiles(folder); + } +} + +QWidget* PreviewBsa::genBsaPreview(const QString& fileName, const QSize&) +{ + m_Files.clear(); + QWidget* wrapper = new QWidget(); + QVBoxLayout* layout = new QVBoxLayout(); + + QLabel* infoLabel = new QLabel(); + BSA::Archive arch; // bs_archive_auto is easier to use, but is less performant when + // working with memory + BSA::EErrorCode res = arch.read(fileName.toLocal8Bit().constData(), true); + if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) { + log::error("invalid bsa '{}', error {}", fileName, res); + infoLabel->setText("Unable to parse archive. Unrecognized format."); + arch.close(); + return wrapper; + } + const BSA::Folder::Ptr archiveDir = arch.getRoot(); + readFiles(archiveDir); + QString infoString = + tr("Archive Format: %1 , Compression: %2 , File count: %3 , Version: %4 , " + "Archive type: %5 , Archive flags: %6"); + // tr("Archive Format: %1 , Compression: %2 , File count: %3 , Version: %4 , " + // "Archive type: %5 , Archive flags: %6 , Contents flags: %7"); + infoString = infoString.arg(getFormat(arch.getType())) + .arg((arch.getFlags() & 0x00000004) ? tr("yes") : tr("no")) + .arg(m_Files.size()) + .arg(getVersion(arch.getType())) + .arg(arch.getType()) + .arg("0x" + QString::number(arch.getFlags(), 16)); + //.arg("0x" + QString::number(arch.get_file_flags(), 16)); + infoLabel->setText(infoString); + layout->addWidget(infoLabel); + + QTreeView* view = new QTreeView(); + SimpleFileTreeModel* model = new SimpleFileTreeModel(m_Files, view); + + view->setModel(model); + layout->addWidget(view); + + QLineEdit* lineEdit = new QLineEdit(); + layout->addWidget(lineEdit); + + model->setFilterWidgetEdit(lineEdit); + model->setFilterWidgetList(view); + + view->setSortingEnabled(true); + view->sortByColumn(0, Qt::SortOrder::AscendingOrder); + + wrapper->setLayout(layout); + arch.close(); + return wrapper; +} + +QString PreviewBsa::getFormat(ArchiveType type) const +{ + switch (type) { + case ArchiveType::TYPE_MORROWIND: + return "Morrowind"; + case ArchiveType::TYPE_OBLIVION: + return "Oblivion"; + case ArchiveType::TYPE_FALLOUT3: + return "Fallout 3, New Vegas, Skyrim LE"; + case ArchiveType::TYPE_SKYRIMSE: + return "Skyrim Special Edition"; + case ArchiveType::TYPE_FALLOUT4: + return "Fallout 4"; + case ArchiveType::TYPE_STARFIELD: + return "Starfield"; + case ArchiveType::TYPE_STARFIELD_LZ4_TEXTURE: + return "Starfield DDS LZ4"; + case ArchiveType::TYPE_FALLOUT4NG_7: + case ArchiveType::TYPE_FALLOUT4NG_8: + return "Fallout 4 NextGen"; + default: + throw data_invalid_exception(makeString("invalid type %d", type)); + } +} + +BSAULong PreviewBsa::getVersion(ArchiveType type) const +{ + switch (type) { + case TYPE_MORROWIND: + return 0x100; + case TYPE_OBLIVION: + return 0x67; + case TYPE_FALLOUT3: + return 0x68; + case TYPE_SKYRIMSE: + return 0x69; + case TYPE_FALLOUT4: + return 0x01; + case TYPE_STARFIELD: + return 0x02; + case TYPE_STARFIELD_LZ4_TEXTURE: + return 0x03; + case TYPE_FALLOUT4NG_7: + return 0x07; + case TYPE_FALLOUT4NG_8: + return 0x08; + default: + throw data_invalid_exception(makeString("invalid type %d", type)); + } +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(previewBsa, PreviewBsa) +#endif diff --git a/libs/preview_bsa/src/previewbsa.h b/libs/preview_bsa/src/previewbsa.h new file mode 100644 index 0000000..6c3ee2e --- /dev/null +++ b/libs/preview_bsa/src/previewbsa.h @@ -0,0 +1,69 @@ +/* +Copyright (C) 2014 Sebastian Herbord. All rights reserved. + +This file is part of bsa Preview plugin for MO + +bsa Preview plugin is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +bsa Preview plugin is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with bsa Preview plugin. If not, see . +*/ + +#ifndef PREVIEWBSA_H +#define PREVIEWBSA_H + +#include + +#include + +#include + +class PreviewBsa : public MOBase::IPluginPreview +{ + + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginPreview) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.PreviewBsa" FILE "previewbsa.json") +#endif + +public: + PreviewBsa(); + +public: + virtual bool init(MOBase::IOrganizer* moInfo); + virtual QString name() const; + virtual QString localizedName() const; + virtual QString author() const; + virtual QString description() const; + virtual MOBase::VersionInfo version() const; + virtual QList settings() const; + +public: + virtual std::set supportedExtensions() const; + virtual QWidget* genFilePreview(const QString& fileName, const QSize& maxSize) const; + +private: + void readFiles(const BSA::Folder::Ptr folder); + QWidget* genBsaPreview(const QString& fileName, const QSize& maxSize); + QString getFormat(ArchiveType type) const; + BSAULong getVersion(ArchiveType type) const; + +private: + std::map> + m_PreviewGenerators; + +private: + const MOBase::IOrganizer* m_MOInfo; + QStringList m_Files; +}; + +#endif // PREVIEWBSA_H diff --git a/libs/preview_bsa/src/previewbsa.json b/libs/preview_bsa/src/previewbsa.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/preview_bsa/src/previewbsa.json @@ -0,0 +1 @@ +{} diff --git a/libs/preview_bsa/src/simplefiletreeitem.cpp b/libs/preview_bsa/src/simplefiletreeitem.cpp new file mode 100644 index 0000000..5bcc5d4 --- /dev/null +++ b/libs/preview_bsa/src/simplefiletreeitem.cpp @@ -0,0 +1,72 @@ + + +/* + simplefiletreeitem.cpp + + A container for items of data supplied by the simple file tree model. +*/ + +#include "simplefiletreeitem.h" + +SimpleFileTreeItem::SimpleFileTreeItem(const QVector& data, + SimpleFileTreeItem* parent) + : m_itemData(data), m_parentItem(parent) +{} + +SimpleFileTreeItem::~SimpleFileTreeItem() +{ + qDeleteAll(m_childItems); +} + +void SimpleFileTreeItem::appendChild(const QString& name, SimpleFileTreeItem* item) +{ + m_childItems.append(item); + m_childItemsByName.insert(name, item); +} + +SimpleFileTreeItem* SimpleFileTreeItem::child(int row) +{ + if (row < 0 || row >= m_childItems.size()) + return nullptr; + return m_childItems.at(row); +} + +SimpleFileTreeItem* SimpleFileTreeItem::childByName(const QString& name) +{ + return m_childItemsByName.value(name); +} + +QVector SimpleFileTreeItem::children() +{ + return m_childItems; +} + +int SimpleFileTreeItem::childCount() const +{ + return m_childItems.count(); +} + +int SimpleFileTreeItem::columnCount() const +{ + return m_itemData.count(); +} + +QVariant SimpleFileTreeItem::data(int column) const +{ + if (column < 0 || column >= m_itemData.size()) + return QVariant(); + return m_itemData.at(column); +} + +SimpleFileTreeItem* SimpleFileTreeItem::parentItem() +{ + return m_parentItem; +} + +int SimpleFileTreeItem::row() const +{ + if (m_parentItem) + return m_parentItem->m_childItems.indexOf(const_cast(this)); + + return 0; +} diff --git a/libs/preview_bsa/src/simplefiletreeitem.h b/libs/preview_bsa/src/simplefiletreeitem.h new file mode 100644 index 0000000..3d5ddd7 --- /dev/null +++ b/libs/preview_bsa/src/simplefiletreeitem.h @@ -0,0 +1,32 @@ +#ifndef SIMPLEFILETREEITEM_H +#define SIMPLEFILETREEITEM_H + +#include +#include + +class SimpleFileTreeItem +{ +public: + explicit SimpleFileTreeItem(const QVector& data, + SimpleFileTreeItem* parentItem = nullptr); + ~SimpleFileTreeItem(); + + void appendChild(const QString& name, SimpleFileTreeItem* child); + + SimpleFileTreeItem* child(int row); + SimpleFileTreeItem* childByName(const QString& name); + QVector children(); + int childCount() const; + int columnCount() const; + QVariant data(int column) const; + int row() const; + SimpleFileTreeItem* parentItem(); + +private: + QVector m_childItems; + QHash m_childItemsByName; + QVector m_itemData; + SimpleFileTreeItem* m_parentItem; +}; + +#endif // SIMPLEFILETREEITEM_H diff --git a/libs/preview_bsa/src/simplefiletreemodel.cpp b/libs/preview_bsa/src/simplefiletreemodel.cpp new file mode 100644 index 0000000..46dcaf7 --- /dev/null +++ b/libs/preview_bsa/src/simplefiletreemodel.cpp @@ -0,0 +1,150 @@ + +/* + simplefiletreemodel.cpp + + Provides a simple tree model for files and folders +*/ + +#include "simplefiletreemodel.h" +#include "simplefiletreeitem.h" + +#include +#include +#include + +SimpleFileTreeModel::SimpleFileTreeModel(const QStringList& data, QObject* parent) + : QAbstractItemModel(parent) +{ + QFileIconProvider* provider = new QFileIconProvider(); + m_FolderIcon = provider->icon(QFileIconProvider::Folder); + m_FileIcon = provider->icon(QFileIconProvider::File); + delete provider; + m_RootItem = new SimpleFileTreeItem({tr("File Name")}); + setupModelData(data, m_RootItem); +} + +SimpleFileTreeModel::~SimpleFileTreeModel() +{ + delete m_RootItem; +} + +int SimpleFileTreeModel::columnCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return static_cast(parent.internalPointer())->columnCount(); + return m_RootItem->columnCount(); +} + +QVariant SimpleFileTreeModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + SimpleFileTreeItem* item = static_cast(index.internalPointer()); + + if (role == Qt::DecorationRole) { + if (item->childCount() > 0) { + return m_FolderIcon; + } else { + return m_FileIcon; + } + } + + if (role != Qt::DisplayRole) + return QVariant(); + + return item->data(index.column()); +} + +Qt::ItemFlags SimpleFileTreeModel::flags(const QModelIndex& index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + + return QAbstractItemModel::flags(index); +} + +QVariant SimpleFileTreeModel::headerData(int section, Qt::Orientation orientation, + int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) + return m_RootItem->data(section); + + return QVariant(); +} + +QModelIndex SimpleFileTreeModel::index(int row, int column, + const QModelIndex& parent) const +{ + if (!hasIndex(row, column, parent)) + return QModelIndex(); + + SimpleFileTreeItem* parentItem; + + if (!parent.isValid()) + parentItem = m_RootItem; + else + parentItem = static_cast(parent.internalPointer()); + + SimpleFileTreeItem* childItem = parentItem->child(row); + if (childItem) + return createIndex(row, column, childItem); + return QModelIndex(); +} + +QModelIndex SimpleFileTreeModel::parent(const QModelIndex& index) const +{ + if (!index.isValid()) + return QModelIndex(); + + SimpleFileTreeItem* childItem = + static_cast(index.internalPointer()); + SimpleFileTreeItem* parentItem = childItem->parentItem(); + + if (parentItem == m_RootItem) + return QModelIndex(); + + return createIndex(parentItem->row(), 0, parentItem); +} + +int SimpleFileTreeModel::rowCount(const QModelIndex& parent) const +{ + SimpleFileTreeItem* parentItem; + if (parent.column() > 0) + return 0; + + if (!parent.isValid()) + parentItem = m_RootItem; + else + parentItem = static_cast(parent.internalPointer()); + + return parentItem->childCount(); +} + +void SimpleFileTreeModel::setupModelData(const QStringList& lines, SimpleFileTreeItem*) +{ + for (QString line : lines) { + auto fullPath = QDir::cleanPath(line); + QStringList lineEntries = fullPath.split("/"); + auto currentParent = m_RootItem; + + for (int i = 0; i < lineEntries.count(); i++) { + QString currentEntryName = lineEntries[i]; + + // check if item was already added + SimpleFileTreeItem* currentEntry = currentParent->childByName(currentEntryName); + + // add tree item if not found + if (currentEntry == nullptr) { + QVector columnData; + columnData.reserve(m_ColumnCount); + columnData << currentEntryName; + currentEntry = new SimpleFileTreeItem(columnData, currentParent); + currentParent->appendChild(currentEntryName, currentEntry); + } + + // as we go deeper into the path + currentParent = currentEntry; + } + } +} diff --git a/libs/preview_bsa/src/simplefiletreemodel.h b/libs/preview_bsa/src/simplefiletreemodel.h new file mode 100644 index 0000000..5189d89 --- /dev/null +++ b/libs/preview_bsa/src/simplefiletreemodel.h @@ -0,0 +1,48 @@ +#ifndef SIMPLEFILETREEMODEL_H +#define SIMPLEFILETREEMODEL_H + +#include +#include +#include +#include +#include + +#include + +using namespace MOBase; + +class SimpleFileTreeItem; + +class SimpleFileTreeModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit SimpleFileTreeModel(const QStringList& data, QObject* parent = nullptr); + ~SimpleFileTreeModel(); + + void setFilterWidgetList(QAbstractItemView* list) { m_FilterWidget.setList(list); } + void setFilterWidgetEdit(QLineEdit* edit) { m_FilterWidget.setEdit(edit); } + + QVariant data(const QModelIndex& index, int role) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + QModelIndex index(int row, int column, + const QModelIndex& parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& index) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + +private: + // takes a list of relative file paths assumed with the same base folder and generates + // the filetree model + void setupModelData(const QStringList& lines, SimpleFileTreeItem* parent); + const int m_ColumnCount = 1; + SimpleFileTreeItem* m_RootItem; + FilterWidget m_FilterWidget; + QIcon m_FileIcon; + QIcon m_FolderIcon; +}; + +#endif // SIMPLEFILETREEMODEL_H diff --git a/libs/preview_bsa/vcpkg.json b/libs/preview_bsa/vcpkg.json new file mode 100644 index 0000000..ecb6d49 --- /dev/null +++ b/libs/preview_bsa/vcpkg.json @@ -0,0 +1,37 @@ +{ + "dependencies": [ + "mo2-dds-header", + "zlib", + "lz4", + "mo2-dds-header", + "boost-interprocess", + "boost-thread" + ], + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase", "mo2-bsatk"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43", + "packages": ["boost*", "boost-*"] + }, + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673", + "packages": ["mo2-*", "pybind11", "spdlog"] + } + ] + } +} diff --git a/libs/preview_dds/.editorconfig b/libs/preview_dds/.editorconfig new file mode 100644 index 0000000..f64592a --- /dev/null +++ b/libs/preview_dds/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.py] +indent_style = space +indent_size = 4 + +[{CMakeLists.txt, *.cmake}] +indent_style = space +indent_size = 4 diff --git a/libs/preview_dds/.gitignore b/libs/preview_dds/.gitignore new file mode 100644 index 0000000..5afe969 --- /dev/null +++ b/libs/preview_dds/.gitignore @@ -0,0 +1,109 @@ +# Mod Organizer Umbrella stuff +/msbuild.log +/*std*.log +/*build + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/libs/preview_dds/.pre-commit-config.yaml b/libs/preview_dds/.pre-commit-config.yaml new file mode 100644 index 0000000..4b7d480 --- /dev/null +++ b/libs/preview_dds/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/preview_dds/CMakeLists.txt b/libs/preview_dds/CMakeLists.txt new file mode 100644 index 0000000..a9ad8a8 --- /dev/null +++ b/libs/preview_dds/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(DDSPreview LANGUAGES NONE) + +add_subdirectory(src) diff --git a/libs/preview_dds/CMakePresets.json b/libs/preview_dds/CMakePresets.json new file mode 100644 index 0000000..7726456 --- /dev/null +++ b/libs/preview_dds/CMakePresets.json @@ -0,0 +1,11 @@ +{ + "configurePresets": [ + { + "binaryDir": "${sourceDir}/vsbuild", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "generator": "Visual Studio 17 2022", + "name": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/preview_dds/LICENSE b/libs/preview_dds/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/libs/preview_dds/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/libs/preview_dds/README.md b/libs/preview_dds/README.md new file mode 100644 index 0000000..02ff9a7 --- /dev/null +++ b/libs/preview_dds/README.md @@ -0,0 +1,2 @@ +# modorganizer-preview_dds +DDS preview plugin for Mod Organizer 2. diff --git a/libs/preview_dds/src/CMakeLists.txt b/libs/preview_dds/src/CMakeLists.txt new file mode 100644 index 0000000..f4f0587 --- /dev/null +++ b/libs/preview_dds/src/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(mo2-cmake CONFIG REQUIRED) + +add_custom_target(DDSPreview ALL) +mo2_configure_python(DDSPreview SIMPLE) diff --git a/libs/preview_dds/src/DDS/DDSDefinitions.py b/libs/preview_dds/src/DDS/DDSDefinitions.py new file mode 100644 index 0000000..2d44812 --- /dev/null +++ b/libs/preview_dds/src/DDS/DDSDefinitions.py @@ -0,0 +1,808 @@ +import dataclasses +from enum import IntEnum, IntFlag, auto +import struct +from typing import ClassVar, List + +from .glstuff import GL_IMAGE_FORMAT, CompressedGLTextureFormat, UncompressedGLTextureFormat + +DDS_MAGIC_NUMBER = b"DDS " # b"\x20\x53\x44\x44" + + +class IntEnumFromZero(IntEnum): + def _generate_next_value_(name, start, count, last_values): + return count + + +class D3D10_RESOURCE_DIMENSION(IntEnumFromZero): + D3D10_RESOURCE_DIMENSION_UNKNOWN = auto() + D3D10_RESOURCE_DIMENSION_BUFFER = auto() + D3D10_RESOURCE_DIMENSION_TEXTURE1D = auto() + D3D10_RESOURCE_DIMENSION_TEXTURE2D = auto() + D3D10_RESOURCE_DIMENSION_TEXTURE3D = auto() + + +class DXGI_FORMAT(IntEnumFromZero): + DXGI_FORMAT_UNKNOWN = auto() + DXGI_FORMAT_R32G32B32A32_TYPELESS = auto() + DXGI_FORMAT_R32G32B32A32_FLOAT = auto() + DXGI_FORMAT_R32G32B32A32_UINT = auto() + DXGI_FORMAT_R32G32B32A32_SINT = auto() + DXGI_FORMAT_R32G32B32_TYPELESS = auto() + DXGI_FORMAT_R32G32B32_FLOAT = auto() + DXGI_FORMAT_R32G32B32_UINT = auto() + DXGI_FORMAT_R32G32B32_SINT = auto() + DXGI_FORMAT_R16G16B16A16_TYPELESS = auto() + DXGI_FORMAT_R16G16B16A16_FLOAT = auto() + DXGI_FORMAT_R16G16B16A16_UNORM = auto() + DXGI_FORMAT_R16G16B16A16_UINT = auto() + DXGI_FORMAT_R16G16B16A16_SNORM = auto() + DXGI_FORMAT_R16G16B16A16_SINT = auto() + DXGI_FORMAT_R32G32_TYPELESS = auto() + DXGI_FORMAT_R32G32_FLOAT = auto() + DXGI_FORMAT_R32G32_UINT = auto() + DXGI_FORMAT_R32G32_SINT = auto() + DXGI_FORMAT_R32G8X24_TYPELESS = auto() + DXGI_FORMAT_D32_FLOAT_S8X24_UINT = auto() + DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS = auto() + DXGI_FORMAT_X32_TYPELESS_G8X24_UINT = auto() + DXGI_FORMAT_R10G10B10A2_TYPELESS = auto() + DXGI_FORMAT_R10G10B10A2_UNORM = auto() + DXGI_FORMAT_R10G10B10A2_UINT = auto() + DXGI_FORMAT_R11G11B10_FLOAT = auto() + DXGI_FORMAT_R8G8B8A8_TYPELESS = auto() + DXGI_FORMAT_R8G8B8A8_UNORM = auto() + DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = auto() + DXGI_FORMAT_R8G8B8A8_UINT = auto() + DXGI_FORMAT_R8G8B8A8_SNORM = auto() + DXGI_FORMAT_R8G8B8A8_SINT = auto() + DXGI_FORMAT_R16G16_TYPELESS = auto() + DXGI_FORMAT_R16G16_FLOAT = auto() + DXGI_FORMAT_R16G16_UNORM = auto() + DXGI_FORMAT_R16G16_UINT = auto() + DXGI_FORMAT_R16G16_SNORM = auto() + DXGI_FORMAT_R16G16_SINT = auto() + DXGI_FORMAT_R32_TYPELESS = auto() + DXGI_FORMAT_D32_FLOAT = auto() + DXGI_FORMAT_R32_FLOAT = auto() + DXGI_FORMAT_R32_UINT = auto() + DXGI_FORMAT_R32_SINT = auto() + DXGI_FORMAT_R24G8_TYPELESS = auto() + DXGI_FORMAT_D24_UNORM_S8_UINT = auto() + DXGI_FORMAT_R24_UNORM_X8_TYPELESS = auto() + DXGI_FORMAT_X24_TYPELESS_G8_UINT = auto() + DXGI_FORMAT_R8G8_TYPELESS = auto() + DXGI_FORMAT_R8G8_UNORM = auto() + DXGI_FORMAT_R8G8_UINT = auto() + DXGI_FORMAT_R8G8_SNORM = auto() + DXGI_FORMAT_R8G8_SINT = auto() + DXGI_FORMAT_R16_TYPELESS = auto() + DXGI_FORMAT_R16_FLOAT = auto() + DXGI_FORMAT_D16_UNORM = auto() + DXGI_FORMAT_R16_UNORM = auto() + DXGI_FORMAT_R16_UINT = auto() + DXGI_FORMAT_R16_SNORM = auto() + DXGI_FORMAT_R16_SINT = auto() + DXGI_FORMAT_R8_TYPELESS = auto() + DXGI_FORMAT_R8_UNORM = auto() + DXGI_FORMAT_R8_UINT = auto() + DXGI_FORMAT_R8_SNORM = auto() + DXGI_FORMAT_R8_SINT = auto() + DXGI_FORMAT_A8_UNORM = auto() + DXGI_FORMAT_R1_UNORM = auto() + DXGI_FORMAT_R9G9B9E5_SHAREDEXP = auto() + DXGI_FORMAT_R8G8_B8G8_UNORM = auto() + DXGI_FORMAT_G8R8_G8B8_UNORM = auto() + DXGI_FORMAT_BC1_TYPELESS = auto() + DXGI_FORMAT_BC1_UNORM = auto() + DXGI_FORMAT_BC1_UNORM_SRGB = auto() + DXGI_FORMAT_BC2_TYPELESS = auto() + DXGI_FORMAT_BC2_UNORM = auto() + DXGI_FORMAT_BC2_UNORM_SRGB = auto() + DXGI_FORMAT_BC3_TYPELESS = auto() + DXGI_FORMAT_BC3_UNORM = auto() + DXGI_FORMAT_BC3_UNORM_SRGB = auto() + DXGI_FORMAT_BC4_TYPELESS = auto() + DXGI_FORMAT_BC4_UNORM = auto() + DXGI_FORMAT_BC4_SNORM = auto() + DXGI_FORMAT_BC5_TYPELESS = auto() + DXGI_FORMAT_BC5_UNORM = auto() + DXGI_FORMAT_BC5_SNORM = auto() + DXGI_FORMAT_B5G6R5_UNORM = auto() + DXGI_FORMAT_B5G5R5A1_UNORM = auto() + DXGI_FORMAT_B8G8R8A8_UNORM = auto() + DXGI_FORMAT_B8G8R8X8_UNORM = auto() + DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM = auto() + DXGI_FORMAT_B8G8R8A8_TYPELESS = auto() + DXGI_FORMAT_B8G8R8A8_UNORM_SRGB = auto() + DXGI_FORMAT_B8G8R8X8_TYPELESS = auto() + DXGI_FORMAT_B8G8R8X8_UNORM_SRGB = auto() + DXGI_FORMAT_BC6H_TYPELESS = auto() + DXGI_FORMAT_BC6H_UF16 = auto() + DXGI_FORMAT_BC6H_SF16 = auto() + DXGI_FORMAT_BC7_TYPELESS = auto() + DXGI_FORMAT_BC7_UNORM = auto() + DXGI_FORMAT_BC7_UNORM_SRGB = auto() + DXGI_FORMAT_AYUV = auto() + DXGI_FORMAT_Y410 = auto() + DXGI_FORMAT_Y416 = auto() + DXGI_FORMAT_NV12 = auto() + DXGI_FORMAT_P010 = auto() + DXGI_FORMAT_P016 = auto() + DXGI_FORMAT_420_OPAQUE = auto() + DXGI_FORMAT_YUY2 = auto() + DXGI_FORMAT_Y210 = auto() + DXGI_FORMAT_Y216 = auto() + DXGI_FORMAT_NV11 = auto() + DXGI_FORMAT_AI44 = auto() + DXGI_FORMAT_IA44 = auto() + DXGI_FORMAT_P8 = auto() + DXGI_FORMAT_A8P8 = auto() + DXGI_FORMAT_B4G4R4A4_UNORM = auto() + DXGI_FORMAT_P208 = auto() + DXGI_FORMAT_V208 = auto() + DXGI_FORMAT_V408 = auto() + DXGI_FORMAT_FORCE_UINT = auto() + + +def DataclassFromBytes(dataclass): + class LoadableDataclass(dataclass): + def __init__(self, bytes=None): + super(LoadableDataclass, self).__init__() + if bytes: + self.fromBytes(bytes) + + def fromStream(self, byteStream): + self.fromBytes(byteStream.read(struct.calcsize(self.structFormat))) + + def fromBytes(self, bytes): + loaded = struct.unpack(self.structFormat, bytes) + fields = dataclasses.fields(self) + memberIndex = 0 + for field in fields: + if field.metadata and "count" in field.metadata: + # We have a list + listed = field.type.__args__[0] + myList = [] + for i in range(field.metadata["count"]): + myList.append(listed(loaded[memberIndex])) + memberIndex += 1 + self.__dict__[field.name] = myList + else: + self.__dict__[field.name] = field.type(loaded[memberIndex]) + memberIndex += 1 + + return LoadableDataclass + + +@DataclassFromBytes +@dataclasses.dataclass +class DDS_PIXELFORMAT: + structFormat: ClassVar[str] = "> rShift) * multiplier) // rDivisor + else: + red = 0 + if gIntMask: + green = (((gIntMask & pixel) >> gShift) * multiplier) // gDivisor + else: + green = 0 + if bIntMask: + blue = (((bIntMask & pixel) >> bShift) * multiplier) // bDivisor + else: + blue = 0 + if aIntMask: + alpha = (((aIntMask & pixel) >> aShift) * multiplier) // aDivisor + else: + alpha = multiplier + struct.pack_into(packFormat, repacked, repackIndex, blue, green, red, alpha) + repackIndex += biggestComponent * 4 + return bytes(repacked) + + return (convert, glInternalFormat, glFormat, glType) + + +def getGLFormat(pixelFormat, dxt10Header=None): + # Half or more of this function may be unreachable or otherwise redundant. + glInternalFormat = None + dxgiFormat = None + if dxt10Header: + dxgiFormat = dxt10Header.dxgiFormat + flags = pixelFormat.dwFlags + if flags & pixelFormat.Flags.DDPF_FOURCC: + fourCC = pixelFormat.dwFourCC + if fourCC == b"DX10" and not dxt10Header: + raise UnsupportedDDSFormatException() + dxgiFormat = dxt10Header.dxgiFormat if dxt10Header else fourCCToDXGI(fourCC) + + if dxgiFormat: + return dxgiToGL[dxgiFormat] + elif flags & ( + pixelFormat.Flags.DDPF_ALPHA | pixelFormat.Flags.DDPF_RGB | pixelFormat.Flags.DDPF_YUV | pixelFormat.Flags.DDPF_LUMINANCE): + compressed = False + + rBitmask = None + gBitmask = None + bBitmask = None + aBitmask = None + lumBitmask = None + if flags & (pixelFormat.Flags.DDPF_ALPHA | pixelFormat.Flags.DDPF_ALPHAPIXELS): + aBitmask = pixelFormat.dwABitMask + if flags & (pixelFormat.Flags.DDPF_RGB | pixelFormat.Flags.DDPF_YUV): + rBitmask = pixelFormat.dwRBitMask + gBitmask = pixelFormat.dwGBitMask + bBitmask = pixelFormat.dwBBitMask + if flags & pixelFormat.Flags.DDPF_LUMINANCE: + lumBitmask = pixelFormat.dwRBitMask + + def bitCount(theBytes): + count = 0 + for byte in theBytes: + count += bin(byte).count("1") + return count + + def firstBit(theBytes): + index = 0 + for byte in theBytes: + if byte != 0: + return index + format(byte, 'b').find("1") + index += 8 + + bitCounts = dict() + starts = dict() + namedBitmasks = dict() + if rBitmask: + bitCounts["r"] = bitCount(rBitmask) + starts["r"] = firstBit(rBitmask) + namedBitmasks["r"] = rBitmask + if gBitmask: + bitCounts["g"] = bitCount(gBitmask) + starts["g"] = firstBit(gBitmask) + namedBitmasks["g"] = gBitmask + if bBitmask: + bitCounts["b"] = bitCount(bBitmask) + starts["b"] = firstBit(bBitmask) + namedBitmasks["b"] = bBitmask + if aBitmask: + bitCounts["a"] = bitCount(aBitmask) + starts["a"] = firstBit(aBitmask) + namedBitmasks["a"] = aBitmask + if lumBitmask: + bitCounts["luminance"] = bitCount(lumBitmask) + starts["luminance"] = firstBit(lumBitmask) + namedBitmasks["luminance"] = lumBitmask + + toSort = [] + for key in starts: + toSort.append((starts[key], key)) + toSort.sort() + + glInternalFormatName = ["GL"] + glRequirements = None + + desc = "GL_" + lastBitCount = -1 + usedBitCounts = set() + for pos, channel in toSort: + if lastBitCount != bitCounts[channel]: + if lastBitCount != -1: + desc += str(lastBitCount) + lastBitCount = bitCounts[channel] + usedBitCounts.add(lastBitCount) + desc += channel + if len(usedBitCounts) != 1: + desc += str(lastBitCount) + + desc = desc.upper() + glFormat = GL_IMAGE_FORMAT[desc] if desc in GL_IMAGE_FORMAT.__members__ else None + + numComponents = len(toSort) + byteCount = (pixelFormat.dwRGBBitCount + 7) // 8 + needsConversion = False + if len(usedBitCounts) == 1: + if lastBitCount == 8: + glType = GL_IMAGE_FORMAT.GL_UNSIGNED_BYTE + elif lastBitCount == 16: + glType = GL_IMAGE_FORMAT.GL_UNSIGNED_SHORT + elif lastBitCount == 32: + glType = GL_IMAGE_FORMAT.GL_UNSIGNED_INT + elif lastBitCount == 4: + if numComponents == 4: + glType = GL_IMAGE_FORMAT.GL_UNSIGNED_SHORT_4_4_4_4_REV + else: + needsConversion = True + if not glFormat or numComponents * lastBitCount != pixelFormat.dwRGBBitCount: + needsConversion = True + + if needsConversion: + convert, glInternalFormat, glFormat, glType = buildConverter(byteCount, bitmasks=namedBitmasks) + else: + convert = None + + if not glInternalFormat: + glInternalFormatName = "_".join(glInternalFormatName) + glInternalFormat = GL_IMAGE_FORMAT[ + glInternalFormatName] if glInternalFormatName in GL_IMAGE_FORMAT.__members__ else GL_IMAGE_FORMAT.GL_RGBA + + if compressed: + return CompressedGLTextureFormat(glRequirements, glInternalFormat) + else: + return UncompressedGLTextureFormat(glRequirements, glInternalFormat, glFormat, glType, convert) + + +def sizeFromFormat(dxgiFormat, width, height): + blockCompressed = False + if dxgiFormat in {DXGI_FORMAT.DXGI_FORMAT_BC1_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB, + DXGI_FORMAT.DXGI_FORMAT_BC4_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM}: + blockCompressed = True + blockSize = 8 + elif dxgiFormat in {DXGI_FORMAT.DXGI_FORMAT_BC2_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB, + DXGI_FORMAT.DXGI_FORMAT_BC3_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB, + DXGI_FORMAT.DXGI_FORMAT_BC5_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM, + DXGI_FORMAT.DXGI_FORMAT_BC6H_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC6H_UF16, + DXGI_FORMAT.DXGI_FORMAT_BC6H_SF16, + DXGI_FORMAT.DXGI_FORMAT_BC7_TYPELESS, + DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM_SRGB}: + blockCompressed = True + blockSize = 16 + + if blockCompressed: + return max(1, ((width + 3) // 4)) * max(1, ((height + 3) // 4)) * blockSize + + if dxgiFormat <= DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB: + name = dxgiFormat.name + "_" + count = 0 + currentNum = "" + for char in name: + if char.isdecimal(): + currentNum += char + elif currentNum != "": + count += int(currentNum) + currentNum = "" + pixelSize = (count + 7) // 8 + return width * height * pixelSize + pass diff --git a/libs/preview_dds/src/DDS/DDSFile.py b/libs/preview_dds/src/DDS/DDSFile.py new file mode 100644 index 0000000..4592074 --- /dev/null +++ b/libs/preview_dds/src/DDS/DDSFile.py @@ -0,0 +1,220 @@ +import io + +from PyQt6.QtCore import QCoreApplication, qCritical, QFile, QIODeviceBase +from PyQt6.QtOpenGL import QOpenGLTexture + +from . import DDSDefinitions +from .glstuff import GLTextureFormat + + +class DDSReadException(Exception): + """Thrown if there was an error reading a DDS file""" + pass + + +ddsCubemapFaces = { + DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP_POSITIVEX: QOpenGLTexture.CubeMapFace.CubeMapPositiveX, + DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP_NEGATIVEX: QOpenGLTexture.CubeMapFace.CubeMapNegativeX, + DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP_POSITIVEY: QOpenGLTexture.CubeMapFace.CubeMapPositiveY, + DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP_NEGATIVEY: QOpenGLTexture.CubeMapFace.CubeMapNegativeY, + DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP_POSITIVEZ: QOpenGLTexture.CubeMapFace.CubeMapPositiveZ, + DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP_NEGATIVEZ: QOpenGLTexture.CubeMapFace.CubeMapNegativeZ} + + +class DDSFile: + def __init__(self, fileData: bytes, fileName: str): + self.fileName = fileName + self.header = DDSDefinitions.DDS_HEADER() + self.dxt10Header = None + self.glFormat: GLTextureFormat = None + self.fileData = fileData + self.data = None + self.isCubemap = None + + @classmethod + def fromFile(cls, fileName: str): + file = QFile(fileName) + if file.open(QIODeviceBase.OpenModeFlag.ReadOnly): + fileData = file.readAll() + else: + raise DDSReadException() + return cls(fileData.data(), fileName) + + def load(self): + with io.BytesIO(self.fileData) as file: + magicNumber = file.read(4) + if magicNumber != DDSDefinitions.DDS_MAGIC_NUMBER: + qCritical(self.tr("Magic number mismatch.")) + raise DDSReadException() + + self.header.fromStream(file) + + if self.header.ddspf.dwFlags & DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_FOURCC: + fourCC = self.header.ddspf.dwFourCC + if fourCC == b"DX10": + self.dxt10Header = DDSDefinitions.DDS_HEADER_DXT10() + self.dxt10Header.fromStream(file) + else: + fourCC = None + + self.glFormat = DDSDefinitions.getGLFormat(self.header.ddspf, self.dxt10Header) + self.data = [] + # Do this once per layer/mip level whatever, (times one per scanline if uncompressed). Also, potentially recompute this based on the format and size in case writers lie. + # self.data.append(file.read(self.header.dwPitchOrLinearSize)) + + layerCount = 1 + if self.header.dwCaps2 & DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP: + self.isCubemap = True + layerCount = 0 + for face in ddsCubemapFaces: + if self.header.dwCaps2 & face: + layerCount += 1 + else: + self.isCubemap = False + + for layer in range(layerCount): + nextWidth = self.header.dwWidth + nextHeight = self.header.dwHeight + mipCount = self.mipLevels() + for level in range(mipCount): + if self.header.ddspf.dwFlags & ( + DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_ALPHA | DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_RGB | DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_YUV | DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_LUMINANCE): + size = nextWidth * nextHeight * ((self.header.ddspf.dwRGBBitCount + 7) // 8) + elif fourCC: + if self.dxt10Header: + dxgiFormat = self.dxt10Header.dxgiFormat + else: + dxgiFormat = DDSDefinitions.fourCCToDXGI(fourCC) + size = DDSDefinitions.sizeFromFormat(dxgiFormat, nextWidth, nextHeight) + self.data.append(file.read(size)) + nextWidth = max(nextWidth // 2, 1) + nextHeight = max(nextHeight // 2, 1) + + def getDescription(self): + format = "" + # DX10 header says the format enum + if self.dxt10Header is not None: + format = self.dxt10Header.dxgiFormat.name.replace("DXGI_FORMAT_", "") + # Pixel Format says the FourCC + elif self.header.ddspf.dwFlags & DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_FOURCC: + fourCC = self.header.ddspf.dwFourCC + format = self.tr("{0} (equivalent to {1})").format(fourCC.decode('ascii'), + DDSDefinitions.fourCCToDXGI(fourCC).name.replace( + "DXGI_FORMAT_", "")) + # We've got bitmasks for the colour channels + else: + # This could be prettier if there was logic to detect that certain common bitmasks represented things more easily represented, like RGBA8 + if self.header.ddspf.dwFlags & ( + DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_RGB | DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_YUV): + format += self.tr("Red bitmask {0}, Green bitmask {1}, Blue bitmask {2}").format( + self.header.ddspf.dwRBitMask.hex().upper(), self.header.ddspf.dwGBitMask.hex().upper(), + self.header.ddspf.dwBBitMask.hex().upper()) + if self.header.ddspf.dwFlags & DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_LUMINANCE: + if format != "": + format += ", " + format += self.tr("Luminance bitmask {0}").format(self.header.ddspf.dwRBitMask.hex().upper()) + if self.header.ddspf.dwFlags & ( + DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_ALPHA | DDSDefinitions.DDS_PIXELFORMAT.Flags.DDPF_ALPHAPIXELS): + if format != "": + format += ", " + format += self.tr("Alpha bitmask {0}").format(self.header.ddspf.dwABitMask.hex().upper()) + + size = self.tr("{0}×{1}").format(self.header.dwWidth, self.header.dwHeight) + + dimensions = self.tr("Cubemap") if self.isCubemap else self.tr("2D") + + mipmaps = self.tr("Mipmapped") if self.mipLevels() != 1 else self.tr("No mipmaps") + + return self.tr("{0}, {1} {2}, {3}").format(format, size, dimensions, mipmaps) + + def mipLevels(self): + if self.header.dwFlags & DDSDefinitions.DDS_HEADER.Flags.DDSD_MIPMAPCOUNT: + return self.header.dwMipMapCount + else: + return 1 + + def asQOpenGLTexture(self, gl, context): + if not self.data: + return + + if self.glFormat.requirements: + minVersion, extensions = self.glFormat.requirements + glVersion = (gl.glGetIntegerv(gl.GL_MAJOR_VERSION), gl.glGetIntegerv(gl.GL_MINOR_VERSION)) + if glVersion < minVersion or minVersion < (1, 0): + compatible = False + for extension in extensions: + if context.hasExtension(extension): + compatible = True + break + if not compatible: + qCritical(self.tr("OpenGL driver incompatible with texture format.")) + return None + + if self.header.dwCaps2 & DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP: + texture = QOpenGLTexture(QOpenGLTexture.Target.TargetCubeMap) + if self.header.dwWidth != self.header.dwHeight: + qCritical(self.tr("Cubemap faces must be square")) + return None + else: + # Assume GL_TEXTURE_2D for now + texture = QOpenGLTexture(QOpenGLTexture.Target.Target2D) + # Assume single layer for now + # self.texture.setLayers(1) + mipCount = self.mipLevels() + texture.setAutoMipMapGenerationEnabled(False) + texture.setMipLevels(mipCount) + texture.setMipLevelRange(0, mipCount - 1) + texture.setSize(self.header.dwWidth, self.header.dwHeight) + texture.setFormat(QOpenGLTexture.TextureFormat(self.glFormat.internalFormat)) + texture.allocateStorage() + + if self.header.dwCaps2 & DDSDefinitions.DDS_HEADER.Caps2.DDSCAPS2_CUBEMAP: + # Lisa hasn't whipped David Wang into shape yet. At least there are fewer bugs than under Raja. + # The specific bug has been reported and AMD "will try to reproduce it soon" + # MO 2.5.0: Radeon-specific code is causing crashing on the latest drivers + # Some cubemaps fail to render with or without these modifications + # noDSA = "Radeon" in gl.glGetString(gl.GL_RENDERER) and self.glFormat.compressed + noDSA = False + if noDSA: + texture.bind() + faceIndex = 0 + for face in ddsCubemapFaces: + if self.header.dwCaps2 & face: + for i in range(mipCount): + if self.glFormat.compressed: + if not noDSA: + texture.setCompressedData(i, 0, ddsCubemapFaces[face], + len(self.data[faceIndex * mipCount + i]), + self.data[faceIndex * mipCount + i]) + else: + gl.glCompressedTexSubImage2D(ddsCubemapFaces[face], i, 0, 0, + max(self.header.dwWidth // 2 ** i, 1), + max(self.header.dwHeight // 2 ** i, 1), + self.glFormat.internalFormat, + len(self.data[faceIndex * mipCount + i]), + self.data[faceIndex * mipCount + i]) + else: + texture.setData(i, 0, ddsCubemapFaces[face], QOpenGLTexture.PixelFormat(self.glFormat.format), QOpenGLTexture.PixelType(self.glFormat.type), + self.glFormat.converter(self.data[faceIndex * mipCount + i])) + faceIndex += 1 + if noDSA: + texture.release() + else: + for i in range(mipCount): + if self.glFormat.compressed: + texture.setCompressedData(i, 0, len(self.data[i]), self.data[i]) + else: + + texture.setData(i, 0, QOpenGLTexture.PixelFormat(self.glFormat.format), QOpenGLTexture.PixelType(self.glFormat.type), + self.glFormat.converter(self.data[i])) + + texture.setWrapMode(QOpenGLTexture.WrapMode.ClampToEdge) + + if self.glFormat.samplerType != "F": + # integer textures can't be filtered + texture.setMinMagFilters(QOpenGLTexture.Filter.NearestMipMapNearest, QOpenGLTexture.Filter.Nearest) + + return texture + + def tr(self, str): + return QCoreApplication.translate("DDSFile", str) diff --git a/libs/preview_dds/src/DDS/__init__.py b/libs/preview_dds/src/DDS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/preview_dds/src/DDS/glstuff.py b/libs/preview_dds/src/DDS/glstuff.py new file mode 100644 index 0000000..314d48f --- /dev/null +++ b/libs/preview_dds/src/DDS/glstuff.py @@ -0,0 +1,205 @@ +from enum import IntEnum + + +class GL_IMAGE_FORMAT(IntEnum): + GL_BYTE = 0x1400 + GL_UNSIGNED_BYTE = 0x1401 + GL_SHORT = 0x1402 + GL_UNSIGNED_SHORT = 0x1403 + GL_INT = 0x1404 + GL_UNSIGNED_INT = 0x1405 + GL_FLOAT = 0x1406 + GL_HALF_FLOAT = 0x140B + GL_COLOR_INDEX = 0x1900 + GL_STENCIL_INDEX = 0x1901 + GL_DEPTH_COMPONENT = 0x1902 + GL_RED = 0x1903 + GL_GREEN = 0x1904 + GL_BLUE = 0x1905 + GL_ALPHA = 0x1906 + GL_RGB = 0x1907 + GL_RGBA = 0x1908 + GL_LUMINANCE = 0x1909 + GL_LUMINANCE_ALPHA = 0x190A + GL_BITMAP = 0x1A00 + GL_R3_G3_B2 = 0x2A10 + GL_UNSIGNED_BYTE_3_3_2 = 0x8032 + GL_UNSIGNED_SHORT_4_4_4_4 = 0x8033 + GL_UNSIGNED_SHORT_5_5_5_1 = 0x8034 + GL_UNSIGNED_INT_8_8_8_8 = 0x8035 + GL_UNSIGNED_INT_10_10_10_2 = 0x8036 + GL_ALPHA4 = 0x803B + GL_ALPHA8 = 0x803C + GL_ALPHA12 = 0x803D + GL_ALPHA16 = 0x803E + GL_LUMINANCE4 = 0x803F + GL_LUMINANCE8 = 0x8040 + GL_LUMINANCE12 = 0x8041 + GL_LUMINANCE16 = 0x8042 + GL_LUMINANCE4_ALPHA4 = 0x8043 + GL_LUMINANCE6_ALPHA2 = 0x8044 + GL_LUMINANCE8_ALPHA8 = 0x8045 + GL_LUMINANCE12_ALPHA4 = 0x8046 + GL_LUMINANCE12_ALPHA12 = 0x8047 + GL_LUMINANCE16_ALPHA16 = 0x8048 + GL_INTENSITY = 0x8049 + GL_INTENSITY4 = 0x804A + GL_INTENSITY8 = 0x804B + GL_INTENSITY12 = 0x804C + GL_INTENSITY16 = 0x804D + GL_RGB4 = 0x804F + GL_RGB5 = 0x8050 + GL_RGB8 = 0x8051 + GL_RGB10 = 0x8052 + GL_RGB12 = 0x8053 + GL_RGB16 = 0x8054 + GL_RGBA2 = 0x8055 + GL_RGBA4 = 0x8056 + GL_RGB5_A1 = 0x8057 + GL_RGBA8 = 0x8058 + GL_RGB10_A2 = 0x8059 + GL_RGBA12 = 0x805A + GL_RGBA16 = 0x805B + GL_BGR = 0x80E0 + GL_BGRA = 0x80E1 + GL_DEPTH_COMPONENT16 = 0x81A5 + GL_DEPTH_COMPONENT24 = 0x81A6 + GL_DEPTH_COMPONENT32 = 0x81A7 + GL_COMPRESSED_RED = 0x8225 + GL_COMPRESSED_RG = 0x8226 + GL_RG = 0x8227 + GL_RG_INTEGER = 0x8228 + GL_R8 = 0x8229 + GL_R16 = 0x822A + GL_RG8 = 0x822B + GL_RG16 = 0x822C + GL_R16F = 0x822D + GL_R32F = 0x822E + GL_RG16F = 0x822F + GL_RG32F = 0x8230 + GL_R8I = 0x8231 + GL_R8UI = 0x8232 + GL_R16I = 0x8233 + GL_R16UI = 0x8234 + GL_R32I = 0x8235 + GL_R32UI = 0x8236 + GL_RG8I = 0x8237 + GL_RG8UI = 0x8238 + GL_RG16I = 0x8239 + GL_RG16UI = 0x823A + GL_RG32I = 0x823B + GL_RG32UI = 0x823C + GL_UNSIGNED_BYTE_2_3_3_REV = 0x8362 + GL_UNSIGNED_SHORT_5_6_5 = 0x8363 + GL_UNSIGNED_SHORT_5_6_5_REV = 0x8364 + GL_UNSIGNED_SHORT_4_4_4_4_REV = 0x8365 + GL_UNSIGNED_SHORT_1_5_5_5_REV = 0x8366 + GL_UNSIGNED_INT_8_8_8_8_REV = 0x8367 + GL_UNSIGNED_INT_2_10_10_10_REV = 0x8368 + GL_COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0 + GL_COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1 + GL_COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2 + GL_COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3 + GL_COMPRESSED_ALPHA = 0x84E9 + GL_COMPRESSED_LUMINANCE = 0x84EA + GL_COMPRESSED_LUMINANCE_ALPHA = 0x84EB + GL_COMPRESSED_INTENSITY = 0x84EC + GL_COMPRESSED_RGB = 0x84ED + GL_COMPRESSED_RGBA = 0x84EE + GL_DEPTH_STENCIL = 0x84F9 + GL_RGBA32F = 0x8814 + GL_RGB32F = 0x8815 + GL_RGBA16F = 0x881A + GL_RGB16F = 0x881B + GL_R11F_G11F_B10F = 0x8C3A + GL_UNSIGNED_INT_10F_11F_11F_REV_EXT = 0x8C3B # EXT_packed_float + GL_RGB9_E5 = 0x8C3D + GL_UNSIGNED_INT_5_9_9_9_REV_EXT = 0x8C3E # EXT_texture_shared_exponent + GL_SRGB = 0x8C40 + GL_SRGB8 = 0x8C41 + GL_SRGB_ALPHA = 0x8C42 + GL_SRGB8_ALPHA8 = 0x8C43 + GL_SLUMINANCE_ALPHA = 0x8C44 + GL_SLUMINANCE8_ALPHA8 = 0x8C45 + GL_SLUMINANCE = 0x8C46 + GL_SLUMINANCE8 = 0x8C47 + GL_COMPRESSED_SRGB = 0x8C48 + GL_COMPRESSED_SRGB_ALPHA = 0x8C49 + GL_COMPRESSED_SRGB_S3TC_DXT1_EXT = 0x8C4C + GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT = 0x8C4D + GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT = 0x8C4E + GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT = 0x8C4F + GL_RGBA32UI = 0x8D70 + GL_RGB32UI = 0x8D71 + GL_RGBA16UI = 0x8D76 + GL_RGB16UI = 0x8D77 + GL_RGBA8UI = 0x8D7C + GL_RGB8UI = 0x8D7D + GL_RGBA32I = 0x8D82 + GL_RGB32I = 0x8D83 + GL_RGBA16I = 0x8D88 + GL_RGB16I = 0x8D89 + GL_RGBA8I = 0x8D8E + GL_RGB8I = 0x8D8F + GL_RED_INTEGER = 0x8D94 + GL_RGB_INTEGER = 0x8D98 + GL_RGBA_INTEGER = 0x8D99 + GL_BGR_INTEGER = 0x8D9A + GL_BGRA_INTEGER = 0x8D9B + GL_COMPRESSED_RED_RGTC1 = 0x8DBB + GL_COMPRESSED_SIGNED_RED_RGTC1 = 0x8DBC + GL_COMPRESSED_RG_RGTC2 = 0x8DBD + GL_COMPRESSED_SIGNED_RG_RGTC2 = 0x8DBE + GL_COMPRESSED_RGBA_BPTC_UNORM = 0x8E8C + GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM = 0x8E8D + GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT = 0x8E8E + GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT = 0x8E8F + GL_R8_SNORM = 0x8F94 + GL_RG8_SNORM = 0x8F95 + GL_RGB8_SNORM = 0x8F96 + GL_RGBA8_SNORM = 0x8F97 + GL_R16_SNORM = 0x8F98 + GL_RG16_SNORM = 0x8F99 + GL_RGB16_SNORM = 0x8F9A + GL_RGBA16_SNORM = 0x8F9B + GL_RGB10_A2UI = 0x906F + GL_COMPRESSED_R11_EAC = 0x9270 + GL_COMPRESSED_SIGNED_R11_EAC = 0x9271 + GL_COMPRESSED_RG11_EAC = 0x9272 + GL_COMPRESSED_SIGNED_RG11_EAC = 0x9273 + GL_COMPRESSED_RGB8_ETC2 = 0x9274 + GL_COMPRESSED_SRGB8_ETC2 = 0x9275 + GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276 + GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277 + GL_COMPRESSED_RGBA8_ETC2_EAC = 0x9278 + GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279 + + +class GLTextureFormat: + def __init__(self, requirements, internalFormat, compressed): + self.requirements = requirements + self.internalFormat = internalFormat + self.compressed = compressed + + if internalFormat.name.endswith("UI"): + self.samplerType = "UI" + elif internalFormat.name.endswith("I"): + self.samplerType = "I" + else: + self.samplerType = "F" + + +class CompressedGLTextureFormat(GLTextureFormat): + def __init__(self, requirements, internalFormat): + super().__init__(requirements, internalFormat, True) + + +class UncompressedGLTextureFormat(GLTextureFormat): + def __init__(self, requirements, internalFormat, format, type, converter=None): + super().__init__(requirements, internalFormat, False) + self.format = format + self.type = type + if converter: + self.converter = converter + else: + self.converter = lambda x: x diff --git a/libs/preview_dds/src/DDSPreview.py b/libs/preview_dds/src/DDSPreview.py new file mode 100644 index 0000000..78ffffc --- /dev/null +++ b/libs/preview_dds/src/DDSPreview.py @@ -0,0 +1,528 @@ +import struct +import sys +import threading +import enum + +from PyQt6.QtCore import QCoreApplication, qDebug, Qt, QSize +from PyQt6.QtGui import QColor, QOpenGLContext, QSurfaceFormat, QMatrix4x4, QVector4D +from PyQt6.QtOpenGLWidgets import QOpenGLWidget +from PyQt6.QtWidgets import QGridLayout, QLabel, QPushButton, QWidget, QColorDialog, QComboBox +from PyQt6.QtOpenGL import QOpenGLBuffer, QOpenGLDebugLogger, QOpenGLShader, QOpenGLShaderProgram, QOpenGLTexture, \ + QOpenGLVersionProfile, QOpenGLVertexArrayObject, QOpenGLFunctions_4_1_Core, QOpenGLVersionFunctionsFactory + +from DDS.DDSFile import DDSFile + +if "mobase" not in sys.modules: + import mobase + +vertexShader2D = """ +#version 150 + +uniform float aspectRatioRatio; + +in vec4 position; +in vec2 texCoordIn; + +out vec2 texCoord; + +void main() +{ + texCoord = texCoordIn; + gl_Position = position; + if (aspectRatioRatio >= 1.0) + gl_Position.y /= aspectRatioRatio; + else + gl_Position.x *= aspectRatioRatio; +} +""" + +vertexShaderCube = """ +#version 150 + +uniform float aspectRatioRatio; + +in vec4 position; +in vec2 texCoordIn; + +out vec2 texCoord; + +void main() +{ + texCoord = texCoordIn; + gl_Position = position; +} +""" + +fragmentShaderFloat = """ +#version 150 + +uniform sampler2D aTexture; +uniform mat4 channelMatrix; +uniform vec4 channelOffset; + +in vec2 texCoord; + +void main() +{ + gl_FragData[0] = channelMatrix * texture(aTexture, texCoord) + channelOffset; +} +""" + +fragmentShaderUInt = """ +#version 150 + +uniform usampler2D aTexture; +uniform mat4 channelMatrix; +uniform vec4 channelOffset; + +in vec2 texCoord; + +void main() +{ + // autofilled alpha is 1, so if we have a scaling factor, we need separate ones for luminance and alpha + gl_FragData[0] = channelMatrix * texture(aTexture, texCoord) + channelOffset; +} +""" + +fragmentShaderSInt = """ +#version 150 + +uniform isampler2D aTexture; +uniform mat4 channelMatrix; +uniform vec4 channelOffset; + +in vec2 texCoord; + +void main() +{ + // autofilled alpha is 1, so if we have a scaling factor and offset, we need separate ones for luminance and alpha + gl_FragData[0] = channelMatrix * texture(aTexture, texCoord) + channelOffset; +} +""" + +fragmentShaderCube = """ +#version 150 + +uniform samplerCube aTexture; +uniform mat4 channelMatrix; +uniform vec4 channelOffset; + +in vec2 texCoord; + +const float PI = 3.1415926535897932384626433832795; + +void main() +{ + float theta = -2.0 * PI * texCoord.x; + float phi = PI * texCoord.y; + gl_FragData[0] = channelMatrix * texture(aTexture, vec3(sin(theta) * sin(phi), cos(theta) * sin(phi), cos(phi))) + channelOffset; +} +""" + +transparencyVS = """ +#version 150 + +in vec4 position; + +void main() +{ + gl_Position = position; +} +""" + +transparencyFS = """ +#version 150 + +uniform vec4 backgroundColour; + +void main() +{ + float x = gl_FragCoord.x; + float y = gl_FragCoord.y; + x = mod(x, 16.0); + y = mod(y, 16.0); + gl_FragData[0] = x < 8.0 ^^ y < 8.0 ? vec4(vec3(191.0/255.0), 1.0) : vec4(1.0); + gl_FragData[0].rgb = backgroundColour.rgb * backgroundColour.a + gl_FragData[0].rgb * (1.0 - backgroundColour.a); +} +""" + +vertices = [ + # vertex coordinates texture coordinates + -1.0, -1.0, 0.5, 1.0, 0.0, 1.0, + -1.0, 1.0, 0.5, 1.0, 0.0, 0.0, + 1.0, 1.0, 0.5, 1.0, 1.0, 0.0, + + -1.0, -1.0, 0.5, 1.0, 0.0, 1.0, + 1.0, 1.0, 0.5, 1.0, 1.0, 0.0, + 1.0, -1.0, 0.5, 1.0, 1.0, 1.0, +] + + +class DDSOptions: + def __init__(self, colour: QColor = QColor(0, 0, 0, 0), channelMatrix: QMatrix4x4 = QMatrix4x4(), + channelOffset: QVector4D = QVector4D()): + # QMatrix4x4 with no arguments is the identity matrix, so no channel transformations + # declare member variables with None values + self.backgroundColour = None + self.channelMatrix = None + self.channelOffset = None + # initialize member variables with error checks + self.setBackgroundColour(colour) + self.setChannelMatrix(channelMatrix) + self.setChannelOffset(channelOffset) + + def setBackgroundColour(self, colour: QColor): + if isinstance(colour, QColor) and colour.isValid(): + self.backgroundColour = colour + else: + raise TypeError(str(colour) + " is not a valid QColor object.") + + def getBackgroundColour(self) -> QColor: + return self.backgroundColour + + def getChannelMatrix(self) -> QMatrix4x4: + return self.channelMatrix + + def setChannelMatrix(self, matrix): + self.channelMatrix = QMatrix4x4(matrix) + + def getChannelOffset(self) -> QVector4D: + return self.channelOffset + + def setChannelOffset(self, vector): + self.channelOffset = QVector4D(vector) + + +glVersionProfile = QOpenGLVersionProfile() +glVersionProfile.setVersion(2, 1) + + +class DDSWidget(QOpenGLWidget): + def __init__(self, ddsFile, ddsOptions=DDSOptions(), debugContext=False, parent=None, f=Qt.WindowType(0)): + super(DDSWidget, self).__init__(parent, f) + + self.ddsFile = ddsFile + + self.ddsOptions = ddsOptions + + self.clean = True + + self.logger = None + + self.program = None + self.transparecyProgram = None + self.texture = None + self.vbo = None + self.vao = None + + if debugContext: + format = QSurfaceFormat() + format.setOption(QSurfaceFormat.FormatOption.DebugContext) + self.setFormat(format) + self.logger = QOpenGLDebugLogger(self) + + def __del__(self): + self.cleanup() + + def __dtor__(self): + self.cleanup() + + def initializeGL(self): + if self.logger: + self.logger.initialize() + self.logger.messageLogged.connect( + lambda message: qDebug(self.tr("OpenGL debug message: {0}").format(message.message()))) + self.logger.startLogging() + + gl = QOpenGLVersionFunctionsFactory.get(glVersionProfile) + QOpenGLContext.currentContext().aboutToBeDestroyed.connect(self.cleanup) + + self.clean = False + + fragmentShader = None + vertexShader = vertexShader2D + if self.ddsFile.isCubemap: + fragmentShader = fragmentShaderCube + vertexShader = vertexShaderCube + if QOpenGLContext.currentContext().hasExtension(b"GL_ARB_seamless_cube_map"): + GL_TEXTURE_CUBE_MAP_SEAMLESS = 0x884F + gl.glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS) + elif self.ddsFile.glFormat.samplerType == "F": + fragmentShader = fragmentShaderFloat + elif self.ddsFile.glFormat.samplerType == "UI": + fragmentShader = fragmentShaderUInt + else: + fragmentShader = fragmentShaderSInt + + self.program = QOpenGLShaderProgram(self) + self.program.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Vertex, vertexShader) + self.program.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Fragment, fragmentShader) + self.program.bindAttributeLocation("position", 0) + self.program.bindAttributeLocation("texCoordIn", 1) + self.program.link() + + self.transparecyProgram = QOpenGLShaderProgram(self) + self.transparecyProgram.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Vertex, transparencyVS) + self.transparecyProgram.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Fragment, transparencyFS) + self.transparecyProgram.bindAttributeLocation("position", 0) + self.transparecyProgram.link() + + self.vao = QOpenGLVertexArrayObject(self) + vaoBinder = QOpenGLVertexArrayObject.Binder(self.vao) + + self.vbo = QOpenGLBuffer(QOpenGLBuffer.Type.VertexBuffer) + self.vbo.create() + self.vbo.bind() + + theBytes = struct.pack("%sf" % len(vertices), *vertices) + self.vbo.allocate(theBytes, len(theBytes)) + + gl.glEnableVertexAttribArray(0) + gl.glEnableVertexAttribArray(1) + gl.glVertexAttribPointer(0, 4, gl.GL_FLOAT, False, 6 * 4, 0) + gl.glVertexAttribPointer(1, 2, gl.GL_FLOAT, False, 6 * 4, 4 * 4) + + self.texture = self.ddsFile.asQOpenGLTexture(gl, QOpenGLContext.currentContext()) + + def resizeGL(self, w, h): + aspectRatioTex = self.texture.width() / self.texture.height() if self.texture else 1.0 + aspectRatioWidget = w / h + ratioRatio = aspectRatioTex / aspectRatioWidget + + self.program.bind() + self.program.setUniformValue("aspectRatioRatio", ratioRatio) + self.program.release() + + def paintGL(self): + gl = QOpenGLVersionFunctionsFactory.get(glVersionProfile) + + vaoBinder = QOpenGLVertexArrayObject.Binder(self.vao) + + # Draw checkerboard so transparency is obvious + self.transparecyProgram.bind() + + backgroundColour = self.ddsOptions.getBackgroundColour() + if backgroundColour and backgroundColour.isValid(): + self.transparecyProgram.setUniformValue("backgroundColour", backgroundColour) + + gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6) + + self.transparecyProgram.release() + + self.program.bind() + + if self.texture: + self.texture.bind() + + gl.glEnable(gl.GL_BLEND) + gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) + + self.program.setUniformValue("channelMatrix", self.ddsOptions.getChannelMatrix()) + self.program.setUniformValue("channelOffset", self.ddsOptions.getChannelOffset()) + + gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6) + + if self.texture: + self.texture.release() + self.program.release() + + def cleanup(self): + if not self.clean: + self.makeCurrent() + + self.program = None + self.transparecyProgram = None + if self.texture: + self.texture.destroy() + self.texture = None + self.vbo.destroy() + self.vbo = None + self.vao.destroy() + self.vao = None + + self.doneCurrent() + self.clean = True + + def tr(self, str): + return QCoreApplication.translate("DDSWidget", str) + + +class ColourChannels(enum.Enum): + RGBA = "Colour and Alpha" + RGB = "Colour" + A = "Alpha" + R = "Red" + G = "Green" + B = "Blue" + + +class DDSChannelManager: + def __init__(self, channels: ColourChannels): + self.channels = channels + + def setChannels(self, options: DDSOptions, channels: ColourChannels): + self.channels = channels + + def drawColour(alpha: bool): + colorMatrix = QMatrix4x4() + colorOffset = QVector4D() + if not alpha: + colorMatrix[3, 3] = 0 + colorOffset.setW(1.0) + options.setChannelMatrix(colorMatrix) + options.setChannelOffset(colorOffset) + + def drawGrayscale(channel: ColourChannels): + colorOffset = QVector4D(0, 0, 0, 1) + channelVector = [0, 0, 0, 0] + if channels == ColourChannels.R: + channelVector[0] = 1 + elif channel == ColourChannels.G: + channelVector[1] = 1 + elif channel == ColourChannels.B: + channelVector[2] = 1 + elif channels == ColourChannels.A: + channelVector[3] = 1 + else: + raise ValueError("channel must be a single color channel.") + alphaVector = [0, 0, 0, 0] + colorMatrix = channelVector * 3 + alphaVector + options.setChannelMatrix(colorMatrix) + options.setChannelOffset(colorOffset) + + if channels == ColourChannels.RGBA: + drawColour(True) + elif channels == ColourChannels.RGB: + drawColour(False) + else: + drawGrayscale(channels) + + +class DDSPreview(mobase.IPluginPreview): + + def __init__(self): + super().__init__() + self.__organizer = None + self.options = None + self.channelManager = None + + def init(self, organizer: mobase.IOrganizer): + self.__organizer = organizer + savedColour = QColor(self.pluginSetting("background r"), self.pluginSetting("background g"), + self.pluginSetting("background b"), self.pluginSetting("background a")) + try: + savedChannels = ColourChannels[self.pluginSetting("channels")] + except KeyError: + savedChannels = ColourChannels.RGBA + self.options = DDSOptions(savedColour) + self.channelManager = DDSChannelManager(savedChannels) + self.channelManager.setChannels(self.options, savedChannels) + return True + + def pluginSetting(self, name): + return self.__organizer.pluginSetting(self.name(), name) + + def setPluginSetting(self, name, value): + self.__organizer.setPluginSetting(self.name(), name, value) + + def name(self): + return "DDS Preview Plugin" + + def author(self): + return "AnyOldName3" + + def description(self): + return self.tr("Lets you preview DDS files by actually uploading them to the GPU.") + + def version(self): + return mobase.VersionInfo(1, 0, 1, 0) + + def settings(self): + return [mobase.PluginSetting("log gl errors", self.tr( + "If enabled, log OpenGL errors and debug messages. May decrease performance."), False), + mobase.PluginSetting("background r", self.tr("Red channel of background colour"), 0), + mobase.PluginSetting("background g", self.tr("Green channel of background colour"), 0), + mobase.PluginSetting("background b", self.tr("Blue channel of background colour"), 0), + mobase.PluginSetting("background a", self.tr("Alpha channel of background colour"), 0), + mobase.PluginSetting("channels", self.tr("The colour channels that are displayed."), + ColourChannels.RGBA.name)] + + def supportedExtensions(self) -> set[str]: + return {"dds"} + + def supportsArchives(self) -> bool: + return True + + def genFilePreview(self, fileName: str, maxSize: QSize) -> QWidget: + return self.previewFromDDSFile(DDSFile.fromFile(fileName)) + + def genDataPreview(self, fileData: bytes, fileName: str, maxSize: QSize) -> QWidget: + return self.previewFromDDSFile(DDSFile(fileData, fileName)) + + def previewFromDDSFile(self, ddsFile: DDSFile) -> QWidget: + ddsFile.load() + layout = QGridLayout() + # Image grows before label and button + layout.setRowStretch(0, 1) + # Label grows before button + layout.setColumnStretch(0, 1) + layout.addWidget(self.__makeLabel(ddsFile), 1, 0, 1, 1) + + ddsWidget = DDSWidget(ddsFile, self.options, self.__organizer.pluginSetting(self.name(), "log gl errors")) + layout.addWidget(ddsWidget, 0, 0, 1, 3) + + layout.addWidget(self.__makeColourButton(ddsWidget), 1, 2, 1, 1) + layout.addWidget(self.__makeChannelsButton(ddsWidget), 1, 1, 1, 1) + + widget = QWidget() + widget.setLayout(layout) + return widget + + def tr(self, str): + return QCoreApplication.translate("DDSPreview", str) + + def __makeLabel(self, ddsFile): + label = QLabel(ddsFile.getDescription()) + label.setWordWrap(True) + label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + return label + + def __makeColourButton(self, ddsWidget): + button = QPushButton(self.tr("Pick background colour")) + + def pickColour(unused): + newColour = QColorDialog.getColor(self.options.getBackgroundColour(), button, "Background colour", + QColorDialog.ColorDialogOption.ShowAlphaChannel) + if newColour.isValid(): + self.setPluginSetting("background r", newColour.red()) + self.setPluginSetting("background g", newColour.green()) + self.setPluginSetting("background b", newColour.blue()) + self.setPluginSetting("background a", newColour.alpha()) + self.options.setBackgroundColour(newColour) + ddsWidget.update() + + button.clicked.connect(pickColour) + return button + + def __makeChannelsButton(self, ddsWidget): + listwidget = QComboBox() + channelKeys = [e.name for e in ColourChannels] + channelNames = [e.value for e in ColourChannels] + + listwidget.addItems(channelNames) + listwidget.setCurrentText(self.channelManager.channels.value) + listwidget.setToolTip(self.tr("Select what colour channels are displayed.")) + + listwidget.showEvent = lambda _: listwidget.setCurrentText(self.channelManager.channels.value) + + def onChanged(newIndex): + self.channelManager.setChannels(self.options, ColourChannels[channelKeys[newIndex]]) + self.setPluginSetting("channels", self.channelManager.channels.name) + ddsWidget.update() + + listwidget.currentIndexChanged.connect(onChanged) + return listwidget + + +def createPlugin(): + return DDSPreview() diff --git a/libs/preview_dds/src/DDSPreview_en.ts b/libs/preview_dds/src/DDSPreview_en.ts new file mode 100644 index 0000000..bc6308a --- /dev/null +++ b/libs/preview_dds/src/DDSPreview_en.ts @@ -0,0 +1,128 @@ + + + + + DDSFile + + + Magic number mismatch. + + + + + {0} (equivalent to {1}) + + + + + Red bitmask {0}, Green bitmask {1}, Blue bitmask {2} + + + + + Luminance bitmask {0} + + + + + Alpha bitmask {0} + + + + + {0}×{1} + + + + + Cubemap + + + + + 2D + + + + + Mipmapped + + + + + No mipmaps + + + + + {0}, {1} {2}, {3} + + + + + OpenGL driver incompatible with texture format. + + + + + Cubemap faces must be square + + + + + DDSPreview + + + Lets you preview DDS files by actually uploading them to the GPU. + + + + + If enabled, log OpenGL errors and debug messages. May decrease performance. + + + + + Red channel of background colour + + + + + Green channel of background colour + + + + + Blue channel of background colour + + + + + Alpha channel of background colour + + + + + The colour channels that are displayed. + + + + + Pick background colour + + + + + Select what colour channels are displayed. + + + + + DDSWidget + + + OpenGL debug message: {0} + + + + diff --git a/libs/preview_dds/vcpkg.json b/libs/preview_dds/vcpkg.json new file mode 100644 index 0000000..da01ba4 --- /dev/null +++ b/libs/preview_dds/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "27d8adbfe9e4ce88a875be3a45fadab69869eb60" + } + } +} diff --git a/libs/script_extender_plugin_checker/.editorconfig b/libs/script_extender_plugin_checker/.editorconfig new file mode 100644 index 0000000..97e2498 --- /dev/null +++ b/libs/script_extender_plugin_checker/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.py] +indent_style = space +indent_size = 4 diff --git a/libs/script_extender_plugin_checker/.gitignore b/libs/script_extender_plugin_checker/.gitignore new file mode 100644 index 0000000..5afe969 --- /dev/null +++ b/libs/script_extender_plugin_checker/.gitignore @@ -0,0 +1,109 @@ +# Mod Organizer Umbrella stuff +/msbuild.log +/*std*.log +/*build + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/libs/script_extender_plugin_checker/.pre-commit-config.yaml b/libs/script_extender_plugin_checker/.pre-commit-config.yaml new file mode 100644 index 0000000..e00bc0d --- /dev/null +++ b/libs/script_extender_plugin_checker/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/script_extender_plugin_checker/CMakeLists.txt b/libs/script_extender_plugin_checker/CMakeLists.txt new file mode 100644 index 0000000..54607be --- /dev/null +++ b/libs/script_extender_plugin_checker/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.16) + +project(ScriptExtenderPluginChecker LANGUAGES NONE) +add_subdirectory(src) diff --git a/libs/script_extender_plugin_checker/CMakePresets.json b/libs/script_extender_plugin_checker/CMakePresets.json new file mode 100644 index 0000000..acc6d97 --- /dev/null +++ b/libs/script_extender_plugin_checker/CMakePresets.json @@ -0,0 +1,17 @@ +{ + "configurePresets": [ + { + "binaryDir": "${sourceDir}/vsbuild", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "generator": "Visual Studio 17 2022", + "name": "vs2022-windows" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/script_extender_plugin_checker/LICENSE b/libs/script_extender_plugin_checker/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/libs/script_extender_plugin_checker/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/libs/script_extender_plugin_checker/src/CMakeLists.txt b/libs/script_extender_plugin_checker/src/CMakeLists.txt new file mode 100644 index 0000000..51da5ba --- /dev/null +++ b/libs/script_extender_plugin_checker/src/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(mo2-cmake CONFIG REQUIRED) + +add_custom_target(ScriptExtenderPluginChecker ALL) +mo2_configure_python(ScriptExtenderPluginChecker SIMPLE) diff --git a/libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker.py b/libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker.py new file mode 100644 index 0000000..35498ea --- /dev/null +++ b/libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +import re +from enum import Enum, auto +from pathlib import Path +from typing import Callable, NamedTuple + +import mobase +from PyQt6.QtCore import QCoreApplication, qDebug + + +class PluginMessage: + kUnknownOrigin = "" + + def __init__(self, pluginPath: str | Path, organizer: mobase.IOrganizer): + self._pluginPath = Path(pluginPath) + try: + self._pluginOrigin = organizer.getFileOrigins( + str(self._pluginPath.relative_to(organizer.managedGame().dataDirectory().absolutePath())))[0] + except: + self._pluginOrigin = PluginMessage.kUnknownOrigin + + def successful(self): + return not self.valid() + + def valid(self): + return self._pluginOrigin != PluginMessage.kUnknownOrigin + + def asMessage(self): + return self.tr("{0} ({1}) existed.").format(self._pluginPath.name, self._pluginOrigin) + + def tr(self, value: str): + return QCoreApplication.translate("PluginMessage", value) + + def pluginPath(self): + return self._pluginPath + + messageTypes: list[tuple[re.Pattern[str], Callable[[re.Match[str], mobase.IOrganizer], PluginMessage]]] = [] + + @staticmethod + def registerMessageType( + messageType: tuple[ + re.Pattern[str], Callable[[re.Match[str], mobase.IOrganizer], PluginMessage] + ], + ): + PluginMessage.messageTypes.append(messageType) + + @staticmethod + def PluginMessageFactory(line: str, organizer: mobase.IOrganizer): + for messageType in PluginMessage.messageTypes: + match = messageType[0].fullmatch(line) + if match: + return messageType[1](match, organizer) + return None + + +class NormalPluginMessage(PluginMessage): + def __init__(self, match: re.Match[str], organizer: mobase.IOrganizer): + super().__init__(match.group("pluginPath"), organizer) + self.__infoVersion = int(match.group("infoVersion"), 16) + self.__name = match.group("name") + self.__version = int(match.group("version"), 16) + self.__loadStatus = match.group("loadStatus") + + def successful(self): + return not self.valid() or self.__loadStatus == "loaded correctly" or self.__loadStatus == "no version data" + + def asMessage(self): + return self.tr("{0} version {1} ({2}, {4}) {3}.").format(self.__name, self.__version, self._pluginPath.name, + self.__trLoadStatus(), self._pluginOrigin) + + def __trLoadStatus(self): + # We need to list the possible options so they get detected as translatable strings. + loadStatusTranslations = { + "loaded correctly": self.tr("loaded correctly"), + # Messages taken from SKSE64 2.1.3 + "disabled, address library needs to be updated": self.tr("disabled, address library needs to be updated"), + "disabled, fatal error occurred while loading plugin": self.tr( + "disabled, fatal error occurred while loading plugin"), + "disabled, bad version data": self.tr("disabled, bad version data"), + "disabled, no name specified": self.tr("disabled, no name specified"), + "disabled, unsupported version independence method": self.tr( + "disabled, unsupported version independence method"), + "disabled, incompatible with current runtime version": self.tr( + "disabled, incompatible with current runtime version"), + "disabled, requires newer script extender": self.tr("disabled, requires newer script extender"), + # Legacy messages + "reported as incompatible during query": self.tr("reported as incompatible during query"), + "reported as incompatible during load": self.tr("reported as incompatible during load"), + "disabled, fatal error occurred while checking plugin compatibility": self.tr( + "disabled, fatal error occurred while checking plugin compatibility"), + "disabled, fatal error occurred while querying plugin": self.tr( + "disabled, fatal error occurred while querying plugin"), + } + if self.__loadStatus in loadStatusTranslations: + return loadStatusTranslations[self.__loadStatus] + else: + # There's a blacklist of known broken plugins that get excluded, each with their own reason. + # We aren't translating that. + return self.__loadStatus + + def tr(self, value: str): + return QCoreApplication.translate("NormalPluginMessage", value) + + +PluginMessage.registerMessageType((re.compile( + r"plugin (?P.+) \((?P[\dA-Fa-f]{8}) (?P.*) (?P[\dA-Fa-f]{8})\) (?P.+?)(?P \d+)?( \(handle \d+\))?\s$"), + NormalPluginMessage)) + + +class CouldntLoadPluginMessage(PluginMessage): + def __init__(self, match: re.Match[str], organizer: mobase.IOrganizer): + super().__init__(match.group("pluginPath"), organizer) + self.__lastError = int(match.group("lastError")) + self.__scriptExtenderDetails = match.group("seDetails") + if not self.__scriptExtenderDetails or self.__scriptExtenderDetails.isspace(): + self.__scriptExtenderDetails = "" + + def successful(self): + return not self.valid() + + def asMessage(self): + if self.__lastError == 126: + message = self.tr("Couldn't load {0} ({2}). A dependency DLL could not be found (code {1}). {3}") + elif self.__lastError == 193: + message = self.tr("Couldn't load {0} ({2}). A DLL is invalid (code {1}).") + else: + message = self.tr("Couldn't load {0} ({2}). The last error code was {1}.") + + return message.format(self._pluginPath.name, self.__lastError, self._pluginOrigin, self.__scriptExtenderDetails) + + def tr(self, value: str): + return QCoreApplication.translate("CouldntLoadPluginMessage", value) + + +PluginMessage.registerMessageType((re.compile( + r"couldn't load plugin (?P.+) \(Error (code )?(?P[-+]?\d+)(:\s*(?P.*))?\)\s"), + CouldntLoadPluginMessage)) + + +class NotAPluginMessage(PluginMessage): + def __init__(self, match: re.Match[str], organizer: mobase.IOrganizer): + super().__init__(match.group("pluginPath"), organizer) + + def successful(self): + return not self.valid() + + def asMessage(self): + return self.tr("{0} ({1}) does not appear to be a script extender plugin.").format(self._pluginPath.name, + self._pluginOrigin) + + def tr(self, value: str): + return QCoreApplication.translate("NotAPluginMessage", value) + + +PluginMessage.registerMessageType((re.compile( + r"plugin (?P.+) does not appear to be an (?:SK)|(?:F4)|(?:NV)|(?:FO)|(?:OB)SE plugin\s"), + NotAPluginMessage)) + + +class LogLocation(Enum): + DOCS = auto() + INSTALL = auto() + + +class ScriptExtenderPluginChecker(mobase.IPluginDiagnose): + class GameType(NamedTuple): + base: LogLocation + gameSuffix: Path | None + editorSuffix: Path | None + + supportedGames = { + "Skyrim": GameType(LogLocation.DOCS, Path("SKSE") / "skse.log", Path("SKSE") / "skse_editor.log"), + "Skyrim Special Edition": GameType(LogLocation.DOCS, Path("SKSE") / "skse64.log", None), + # No editor log defined + "Skyrim VR": GameType(LogLocation.DOCS, Path("SKSE") / "sksevr.log", None), # No editor log defined + # "Enderal" : GameType(LogLocation.DOCS, Path("")), + "Fallout 4": GameType(LogLocation.DOCS, Path("F4SE") / "f4se.log", None), # No editor log defined + "Oblivion": GameType(LogLocation.INSTALL, Path("obse.log"), Path("obse_editor.log")), + "New Vegas": GameType(LogLocation.INSTALL, Path("nvse.log"), Path("nvse_editor.log")), + "TTW": GameType(LogLocation.INSTALL, Path("nvse.log"), Path("nvse_editor.log")), + "Fallout 3": GameType(LogLocation.INSTALL, Path("fose.log"), Path("fose_editor.log")) + } + + __organizer: mobase.IOrganizer + + def __init__(self): + super(ScriptExtenderPluginChecker, self).__init__() + + def init(self, organizer: mobase.IOrganizer): + self.__organizer = organizer + + organizer.onFinishedRun(lambda a, b: self._invalidate()) + + return True + + def name(self): + return "Script Extender Plugin Load Checker" + + def localizedName(self): + return self.tr("Script Extender Plugin Load Checker") + + def author(self): + return "AnyOldName3" + + def description(self): + return self.tr("Checks script extender log to see if any plugins failed to load.") + + def version(self): + return mobase.VersionInfo(1, 2, 0, 0) + + def requirements(self) -> list[mobase.IPluginRequirement]: + return [ + mobase.PluginRequirementFactory.gameDependency(list(self.supportedGames)) + ] + + def settings(self) -> list[mobase.PluginSetting]: + return [] + + def activeProblems(self) -> list[int]: + if self.__scanLog(): + return [0] + else: + return [] + + def shortDescription(self, key: int): + return self.tr("Script extender log reports incompatible plugins.") + + def fullDescription(self, key: int): + pluginList = self.__listBadPluginMessagess() + pluginListString = "\n • " + ("\n • ".join(pluginList)) + return self.tr("You have one or more script extender plugins which failed to load!\n\n " + "If you want this notification to go away, here are some steps you can take:\n" + " • Look for updates to the mod or the specific plugin included in the mod.\n" + " • Disable the mod containing the plugin.\n" + " • Hide or delete the plugin from the mod.\n\n" + "To refresh the script extender logs, you will need to run the game and/or editor again!\n\n" + "The failed plugins are:{0}").format(pluginListString) + + def hasGuidedFix(self, key: int): + return False + + def startGuidedFix(self, key: int): + pass + + def tr(self, value: str): + return QCoreApplication.translate("ScriptExtenderPluginChecker", value) + + def __scanLog(self): + return len(self.__listBadPluginMessagess()) > 0 + + def __listBadPluginMessagess(self) -> list[str]: + base, gameSuffix, editorSuffix = self.supportedGames[self.__organizer.managedGame().gameName()] + + if base == LogLocation.DOCS: + base = Path(self.__organizer.managedGame().documentsDirectory().absolutePath()) + elif base == LogLocation.INSTALL: + base = Path(self.__organizer.managedGame().gameDirectory().absolutePath()) + + editorMessageList: list[PluginMessage] = [] + gameMessageList: list[PluginMessage] = [] + + if gameSuffix is not None: + gameLogPath = base / gameSuffix + try: + if gameLogPath.exists(): + with gameLogPath.open('r', encoding='cp1252') as logFile: + for line in logFile: + message = PluginMessage.PluginMessageFactory(line, self.__organizer) + if message: + gameMessageList.append(message) + except Exception as e: + qDebug(str(e)) + # There's almost certainly just no log yet + pass + + if editorSuffix is not None: + editorLogPath = base / editorSuffix + try: + if editorLogPath.exists(): + with editorLogPath.open('r', encoding='cp1252') as logFile: + for line in logFile: + message = PluginMessage.PluginMessageFactory(line, self.__organizer) + if message: + editorMessageList.append(message) + except Exception as e: + qDebug(str(e)) + # There's almost certainly just no log yet + pass + + messageList: list[str] = [] + + # Search each list for plugins that are not successful in either list + for gameMessage in gameMessageList: + if not gameMessage.successful(): + for editorMessage in editorMessageList: + if gameMessage.pluginPath() == editorMessage.pluginPath() and editorMessage.successful(): + break + else: + message = gameMessage.asMessage() + if message not in messageList: + messageList.append(message) + + for editorMessage in editorMessageList: + if not editorMessage.successful(): + for gameMessage in gameMessageList: + if editorMessage.pluginPath() == gameMessage.pluginPath() and gameMessage.successful(): + break + else: + message = editorMessage.asMessage() + if message not in messageList: + messageList.append(message) + + return messageList + + +def createPlugin(): + return ScriptExtenderPluginChecker() diff --git a/libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker_en.ts b/libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker_en.ts new file mode 100644 index 0000000..bbc6872 --- /dev/null +++ b/libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker_en.ts @@ -0,0 +1,138 @@ + + + + + CouldntLoadPluginMessage + + + Couldn't load {0} ({2}). A dependency DLL could not be found (code {1}). {3} + + + + + Couldn't load {0} ({2}). A DLL is invalid (code {1}). + + + + + Couldn't load {0} ({2}). The last error code was {1}. + + + + + NormalPluginMessage + + + {0} version {1} ({2}, {4}) {3}. + + + + + loaded correctly + + + + + disabled, address library needs to be updated + + + + + disabled, fatal error occurred while loading plugin + + + + + disabled, bad version data + + + + + disabled, no name specified + + + + + disabled, unsupported version independence method + + + + + disabled, incompatible with current runtime version + + + + + disabled, requires newer script extender + + + + + reported as incompatible during query + + + + + reported as incompatible during load + + + + + disabled, fatal error occurred while checking plugin compatibility + + + + + disabled, fatal error occurred while querying plugin + + + + + NotAPluginMessage + + + {0} ({1}) does not appear to be a script extender plugin. + + + + + PluginMessage + + + {0} ({1}) existed. + + + + + ScriptExtenderPluginChecker + + + Script Extender Plugin Load Checker + + + + + Checks script extender log to see if any plugins failed to load. + + + + + Script extender log reports incompatible plugins. + + + + + You have one or more script extender plugins which failed to load! + + If you want this notification to go away, here are some steps you can take: + • Look for updates to the mod or the specific plugin included in the mod. + • Disable the mod containing the plugin. + • Hide or delete the plugin from the mod. + +To refresh the script extender logs, you will need to run the game and/or editor again! + +The failed plugins are:{0} + + + + diff --git a/libs/script_extender_plugin_checker/vcpkg.json b/libs/script_extender_plugin_checker/vcpkg.json new file mode 100644 index 0000000..2c96d07 --- /dev/null +++ b/libs/script_extender_plugin_checker/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "27d8adbfe9e4ce88a875be3a45fadab69869eb60" + } + } +} diff --git a/libs/tool_inibakery/.clang-format b/libs/tool_inibakery/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/tool_inibakery/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/tool_inibakery/.gitattributes b/libs/tool_inibakery/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/tool_inibakery/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/tool_inibakery/.gitignore b/libs/tool_inibakery/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/tool_inibakery/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/tool_inibakery/.pre-commit-config.yaml b/libs/tool_inibakery/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/tool_inibakery/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/tool_inibakery/CMakeLists.txt b/libs/tool_inibakery/CMakeLists.txt new file mode 100644 index 0000000..84f2257 --- /dev/null +++ b/libs/tool_inibakery/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(inibakery) + +add_subdirectory(src) diff --git a/libs/tool_inibakery/CMakePresets.json b/libs/tool_inibakery/CMakePresets.json new file mode 100644 index 0000000..16e1e6f --- /dev/null +++ b/libs/tool_inibakery/CMakePresets.json @@ -0,0 +1,60 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "version": 4 +} diff --git a/libs/tool_inibakery/src/CMakeLists.txt b/libs/tool_inibakery/src/CMakeLists.txt new file mode 100644 index 0000000..533adef --- /dev/null +++ b/libs/tool_inibakery/src/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB inibakery_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(inibakery SHARED ${inibakery_SOURCES}) +mo2_configure_plugin(inibakery NO_SOURCES WARNINGS 4) +target_link_libraries(inibakery PRIVATE mo2::uibase) +mo2_install_plugin(inibakery) diff --git a/libs/tool_inibakery/src/inibakery.cpp b/libs/tool_inibakery/src/inibakery.cpp new file mode 100644 index 0000000..bb23492 --- /dev/null +++ b/libs/tool_inibakery/src/inibakery.cpp @@ -0,0 +1,97 @@ +#include "inibakery.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace MOBase; + +bool IniBakery::init(MOBase::IOrganizer* moInfo) +{ + using namespace std::placeholders; + m_MOInfo = moInfo; + m_MOInfo->onAboutToRun([this](const auto& binary) { + return prepareIni(binary); + }); + + return true; +} + +QString IniBakery::name() const +{ + return "INI Bakery"; +} + +QString IniBakery::localizedName() const +{ + return tr("INI Bakery"); +} + +QString IniBakery::author() const +{ + return "Tannin"; +} + +QString IniBakery::description() const +{ + return tr("Manages game ini files"); +} + +MOBase::VersionInfo IniBakery::version() const +{ + return VersionInfo(0, 3, 0, VersionInfo::RELEASE_FINAL); +} + +QList IniBakery::settings() const +{ + return QList(); +} + +QStringList IniBakery::iniFileNames() const +{ + return m_MOInfo->managedGame()->iniFiles(); +} + +bool IniBakery::prepareIni(const QString&) +{ + const auto* features = m_MOInfo->gameFeatures(); + + auto savegames = features->gameFeature(); + if (savegames != nullptr) { + savegames->prepareProfile(m_MOInfo->profile().get()); + } + + auto invalidation = features->gameFeature(); + if (invalidation != nullptr) { + invalidation->prepareProfile(m_MOInfo->profile().get()); + } + + return true; +} + +MappingType IniBakery::mappings() const +{ + MappingType result; + + const IPluginGame* game = m_MOInfo->managedGame(); + + auto profile = m_MOInfo->profile(); + + if (profile->localSettingsEnabled()) { + for (const QString& iniFile : iniFileNames()) { + result.push_back({m_MOInfo->profilePath() + "/" + QFileInfo(iniFile).fileName(), + game->documentsDirectory().absoluteFilePath(iniFile), false, + false}); + } + } + + return result; +} diff --git a/libs/tool_inibakery/src/inibakery.h b/libs/tool_inibakery/src/inibakery.h new file mode 100644 index 0000000..21db29b --- /dev/null +++ b/libs/tool_inibakery/src/inibakery.h @@ -0,0 +1,58 @@ +/* +Mod Organizer ini bakery plugin + +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 +*/ + +#pragma once + +#include +#include +#include + +#include +#include + +/** + * @brief The IniBakery class + */ +class IniBakery : public QObject, + public MOBase::IPlugin, + public MOBase::IPluginFileMapper +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginFileMapper) + Q_PLUGIN_METADATA(IID "org.tannin.IniBakey") + +public: + virtual bool init(MOBase::IOrganizer* moInfo); + virtual QString name() const; + virtual QString localizedName() const; + virtual QString author() const; + virtual QString description() const; + virtual MOBase::VersionInfo version() const; + virtual QList settings() const; + + virtual MappingType mappings() const; + +private: + QStringList iniFileNames() const; + bool prepareIni(const QString& executable); + +private: + MOBase::IOrganizer* m_MOInfo; +}; diff --git a/libs/tool_inibakery/src/inibakery_en.ts b/libs/tool_inibakery/src/inibakery_en.ts new file mode 100644 index 0000000..1617a0b --- /dev/null +++ b/libs/tool_inibakery/src/inibakery_en.ts @@ -0,0 +1,17 @@ + + + + + IniBakery + + + INI Bakery + + + + + Manages game ini files + + + + diff --git a/libs/tool_inibakery/vcpkg.json b/libs/tool_inibakery/vcpkg.json new file mode 100644 index 0000000..486b0fd --- /dev/null +++ b/libs/tool_inibakery/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "d194ec4e14052784518d5e9f1c5ccac16de49c2c" + } + } +} diff --git a/libs/tool_inieditor/.clang-format b/libs/tool_inieditor/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/tool_inieditor/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/tool_inieditor/.gitattributes b/libs/tool_inieditor/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/tool_inieditor/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/tool_inieditor/.gitignore b/libs/tool_inieditor/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/tool_inieditor/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/tool_inieditor/.pre-commit-config.yaml b/libs/tool_inieditor/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/tool_inieditor/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/tool_inieditor/CMakeLists.txt b/libs/tool_inieditor/CMakeLists.txt new file mode 100644 index 0000000..3c843b0 --- /dev/null +++ b/libs/tool_inieditor/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(inieditor) + +add_subdirectory(src) diff --git a/libs/tool_inieditor/CMakePresets.json b/libs/tool_inieditor/CMakePresets.json new file mode 100644 index 0000000..16e1e6f --- /dev/null +++ b/libs/tool_inieditor/CMakePresets.json @@ -0,0 +1,60 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "version": 4 +} diff --git a/libs/tool_inieditor/src/CMakeLists.txt b/libs/tool_inieditor/src/CMakeLists.txt new file mode 100644 index 0000000..3774ee6 --- /dev/null +++ b/libs/tool_inieditor/src/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB inieditor_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(inieditor SHARED ${inieditor_SOURCES}) +mo2_configure_plugin(inieditor NO_SOURCES WARNINGS 4) +target_link_libraries(inieditor PRIVATE mo2::uibase) +mo2_install_plugin(inieditor) diff --git a/libs/tool_inieditor/src/inieditor.cpp b/libs/tool_inieditor/src/inieditor.cpp new file mode 100644 index 0000000..b892137 --- /dev/null +++ b/libs/tool_inieditor/src/inieditor.cpp @@ -0,0 +1,152 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of ini editor plugin for MO + +Ini editor plugin is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ini editor plugin is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Ini editor plugin. If not, see . +*/ + +#include "inieditor.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace MOBase; + +IniEditor::IniEditor() : m_MOInfo(nullptr) {} + +bool IniEditor::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + return true; +} + +QString IniEditor::name() const +{ + return "INI Editor"; +} + +QString IniEditor::localizedName() const +{ + return tr("INI Editor"); +} + +QString IniEditor::author() const +{ + return "Tannin"; +} + +QString IniEditor::description() const +{ + return tr("Very basic editor for INI files"); +} + +VersionInfo IniEditor::version() const +{ + return VersionInfo(1, 1, 0, VersionInfo::RELEASE_FINAL); +} + +QList IniEditor::settings() const +{ + QList result; + result.push_back(PluginSetting("external", "Use an external editor to open the files", + QVariant(false))); + result.push_back(PluginSetting( + "associated", + "When using an external editor, use the application associated with \"INI\" " + "files. " + "If false, uses the \"edit\" command which usually invokes regular notepad.", + QVariant(true))); + return result; +} + +QString IniEditor::displayName() const +{ + return tr("INI Editor"); +} + +QString IniEditor::tooltip() const +{ + return tr("Edit the INI files of the current profile"); +} + +QIcon IniEditor::icon() const +{ + return QIcon(":/inieditor/icon_document"); +} + +void IniEditor::display() const +{ + if (m_MOInfo == nullptr) { + throw MyException(tr("plugin not initialized")); + } + + QStringList iniFiles = m_MOInfo->managedGame()->iniFiles(); + if (m_MOInfo->pluginSetting(name(), "external").toBool()) { + for (QString const& file : iniFiles) { + QString fileName = m_MOInfo->profile()->absoluteIniFilePath(file); +#ifdef _WIN32 + ::ShellExecuteW(nullptr, + m_MOInfo->pluginSetting(name(), "associated").toBool() ? L"open" + : L"edit", + ToWString(fileName).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +#else + QDesktopServices::openUrl(QUrl::fromLocalFile(fileName)); +#endif + } + } else { + TextViewer* viewer = new TextViewer("INI Files", parentWidget()); + if (m_MOInfo->profile()->localSettingsEnabled()) { + viewer->setDescription(tr( + " Editing profile-specific INIs. Profile-specific game INI files are " + "Enabled.\n \n You can change the INI Editor settings (in the main Mod " + "Organizer settings under the plugins tab) to use an external text editor,\n " + "either notepad or your windows associated program to .INI files.")); + } else { + viewer->setDescription(tr( + " Editing global INIs (in 'myGames' folder). Profile-specific game INI files " + "are Disabled.\n \n You can change the INI Editor settings (in the main Mod " + "Organizer settings under the plugins tab) to use an external text editor,\n " + "either notepad or your windows associated program to .INI files.")); + } + for (QString const& file : iniFiles) { + QString fileName = m_MOInfo->profile()->absoluteIniFilePath(file); + QFileInfo fileInfo(fileName); + if (fileInfo.exists()) { + if (fileInfo.size() < 1024 * 1024) { + viewer->addFile(fileName, true); + } else { + QMessageBox::warning(parentWidget(), tr("File too big"), + tr("Sorry, the file %1 is too large" + " to be handled by the INI Editor") + .arg(fileName)); + } + } + } + viewer->show(); + } +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(iniEditor, IniEditor) +#endif diff --git a/libs/tool_inieditor/src/inieditor.h b/libs/tool_inieditor/src/inieditor.h new file mode 100644 index 0000000..fa60ef2 --- /dev/null +++ b/libs/tool_inieditor/src/inieditor.h @@ -0,0 +1,55 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of ini editor plugin for MO + +Ini editor plugin is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ini editor plugin is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Ini editor plugin. If not, see . +*/ + +#ifndef INIEDITOR_H +#define INIEDITOR_H + +#include + +class IniEditor : public MOBase::IPluginTool +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.IniEditor") +#endif + +public: + IniEditor(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + + virtual QString displayName() const; + virtual QString tooltip() const; + virtual QIcon icon() const; + +public slots: + virtual void display() const; + +private: + const MOBase::IOrganizer* m_MOInfo; +}; + +#endif // INIEDITOR_H diff --git a/libs/tool_inieditor/src/inieditor.qrc b/libs/tool_inieditor/src/inieditor.qrc new file mode 100644 index 0000000..e50f7eb --- /dev/null +++ b/libs/tool_inieditor/src/inieditor.qrc @@ -0,0 +1,5 @@ + + + resources/document-properties.png + + diff --git a/libs/tool_inieditor/src/inieditor_en.ts b/libs/tool_inieditor/src/inieditor_en.ts new file mode 100644 index 0000000..db11a1f --- /dev/null +++ b/libs/tool_inieditor/src/inieditor_en.ts @@ -0,0 +1,54 @@ + + + + + IniEditor + + + Very basic editor for INI files + + + + + + INI Editor + + + + + Edit the INI files of the current profile + + + + + plugin not initialized + + + + + Editing profile-specific INIs. Profile-specific game INI files are Enabled. + + You can change the INI Editor settings (in the main Mod Organizer settings under the plugins tab) to use an external text editor, + either notepad or your windows associated program to .INI files. + + + + + File too big + + + + + Sorry, the file %1 is too large to be handled by the INI Editor + + + + + Editing global INIs (in 'myGames' folder). Profile-specific game INI files are Disabled. + + You can change the INI Editor settings (in the main Mod Organizer settings under the plugins tab) to use an external text editor, + either notepad or your windows associated program to .INI files. + + + + diff --git a/libs/tool_inieditor/src/resources/document-properties.png b/libs/tool_inieditor/src/resources/document-properties.png new file mode 100644 index 0000000..a5ad728 Binary files /dev/null and b/libs/tool_inieditor/src/resources/document-properties.png differ diff --git a/libs/tool_inieditor/vcpkg.json b/libs/tool_inieditor/vcpkg.json new file mode 100644 index 0000000..486b0fd --- /dev/null +++ b/libs/tool_inieditor/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "d194ec4e14052784518d5e9f1c5ccac16de49c2c" + } + } +} diff --git a/libs/uibase/.clang-format b/libs/uibase/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/uibase/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/uibase/.git-blame-ignore-revs b/libs/uibase/.git-blame-ignore-revs new file mode 100644 index 0000000..712711a --- /dev/null +++ b/libs/uibase/.git-blame-ignore-revs @@ -0,0 +1 @@ +218607a6ce4d41112bd3995ba319255aec3c5c2e diff --git a/libs/uibase/.gitattributes b/libs/uibase/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/uibase/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/uibase/.github/workflows/build.yml b/libs/uibase/.github/workflows/build.yml new file mode 100644 index 0000000..8d6fbd5 --- /dev/null +++ b/libs/uibase/.github/workflows/build.yml @@ -0,0 +1,79 @@ +name: Build UIBase + +on: + push: + branches: [master] + tags: + - "*" + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }} + +jobs: + build: + runs-on: windows-2022 + steps: + - name: Configure UIBase + id: configure-uibase + uses: ModOrganizer2/build-with-mob-action@master + with: + # skip build because we are going to build both Debug and RelWithDebInfo here + mo2-skip-build: true + + # build both Debug and RelWithDebInfo for package + - name: Build UI Base + working-directory: ${{ steps.configure-uibase.outputs.working-directory }} + run: | + cmake --build vsbuild --config Debug --target uibase-tests --verbose + cmake --build vsbuild --config RelWithDebInfo --target uibase-tests --verbose + + - name: Test UI Base + working-directory: ${{ steps.configure-uibase.outputs.working-directory }} + run: | + ctest --test-dir vsbuild -C Debug --output-on-failure + ctest --test-dir vsbuild -C RelWithDebInfo --output-on-failure + + - name: Install UI Base + working-directory: ${{ steps.configure-uibase.outputs.working-directory }} + run: | + cmake --install vsbuild --config Debug + cmake --install vsbuild --config RelWithDebInfo + + # this tests that UI Base can be properly used as a CMake package + - name: Test UI Base package + run: | + cmake -B build . "-DCMAKE_PREFIX_PATH=${env:QT_ROOT_DIR}\msvc2022_64;${{ github.workspace }}\install\lib\cmake\" + cmake --build build --config Debug + cmake --build build --config Release + cmake --build build --config RelWithDebInfo + working-directory: ${{ steps.configure-uibase.outputs.working-directory }}/tests/cmake + + - name: Upload UI Base artifact + uses: actions/upload-artifact@master + with: + name: uibase + path: ./install + + publish: + if: github.ref_type == 'tag' + needs: build + runs-on: windows-2022 + permissions: + contents: write + steps: + - name: Download Artifact + uses: actions/download-artifact@master + with: + name: uibase + path: ./install + + - name: Create UI Base archive + run: 7z a uibase_${{ github.ref_name }}.7z ./install/* + + - name: Publish Release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh release create --draft=false --notes="${{ github.ref_name }}" "${{ github.ref_name }}" ./uibase_${{ github.ref_name }}.7z diff --git a/libs/uibase/.github/workflows/linting.yml b/libs/uibase/.github/workflows/linting.yml new file mode 100644 index 0000000..67447b2 --- /dev/null +++ b/libs/uibase/.github/workflows/linting.yml @@ -0,0 +1,17 @@ +name: Lint UIBase + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." + exclude-regex: "third-party" diff --git a/libs/uibase/.gitignore b/libs/uibase/.gitignore new file mode 100644 index 0000000..a55b4ba --- /dev/null +++ b/libs/uibase/.gitignore @@ -0,0 +1,9 @@ +edit +.vscode +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build +/src/uibasetests_en.ts +/install +/tests/cmake/build diff --git a/libs/uibase/.pre-commit-config.yaml b/libs/uibase/.pre-commit-config.yaml new file mode 100644 index 0000000..8f53884 --- /dev/null +++ b/libs/uibase/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v20.1.7 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/uibase/CMakeLists.txt b/libs/uibase/CMakeLists.txt new file mode 100644 index 0000000..e9be7bb --- /dev/null +++ b/libs/uibase/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.16) + +include(CMakePackageConfigHelpers) + +project(uibase VERSION 2.5.3) + +# Standalone build - bypass mo2-cmake +add_subdirectory(src) + +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/mo2-uibase-config.cmake" + INSTALL_DESTINATION "lib/cmake/mo2-uibase" + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO +) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/mo2-uibase-config-version.cmake" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY AnyNewerVersion + ARCH_INDEPENDENT +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/mo2-uibase-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/mo2-uibase-config-version.cmake + DESTINATION lib/cmake/mo2-uibase +) + +set(BUILD_TESTING ${BUILD_TESTING} CACHE BOOL "build tests for uibase") +if (BUILD_TESTING) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/libs/uibase/CMakePresets.json b/libs/uibase/CMakePresets.json new file mode 100644 index 0000000..6293933 --- /dev/null +++ b/libs/uibase/CMakePresets.json @@ -0,0 +1,73 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "testing" + } + }, + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone;testing" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/uibase/README.md b/libs/uibase/README.md new file mode 100644 index 0000000..b9abeaa --- /dev/null +++ b/libs/uibase/README.md @@ -0,0 +1,56 @@ +# modorganizer-uibase + +[![Build status](https://github.com/ModOrganizer2/modorganizer-uibase/actions/workflows/build.yml/badge.svg?branch=dev/vcpkg)](https://github.com/ModOrganizer2/modorganizer-uibase/actions) +[![Lint status]](https://github.com/ModOrganizer2/modorganizer-uibase/actions/workflows/linting.yml/badge.svg?branch=dev/vcpkg)]() + +## How to build? + +```pwsh +# set to the appropriate path for Qt +$env:QT_ROOT = "C:\Qt\6.7.0\msvc2019_64" + +# set to the appropriate path for VCPKG +$env:VCPKG_ROOT = "C:\vcpkg" + +cmake --preset vs2022-windows "-DCMAKE_PREFIX_PATH=$env:QT_ROOT" ` + -DCMAKE_INSTALL_PREFIX=install ` + -DBUILD_TESTING=ON + +# build uibase +cmake --build vsbuild --config RelWithDebInfo + +# install uibase +cmake --install vsbuild --config RelWithDebInfo + +# test uibase +ctest --test-dir vsbuild -C RelWithDebInfo --output-on-failure +``` + +Check [`CMakePresets.json`](CMakePresets.json) for some predefined values. Extra options +include: + +- `BUILD_TESTING` - if specified, build tests for UIBase, requires the VCPKG `testing` + feature to be enabled (enabled in the preset). + +## How to use? + +### As a VCPKG dependency + +**Not implemented yet.** + +### As a CMake target + +Once the CMake targets for `uibase` are generated (see _How to build?_), you can include +`mo2::uibase` in your project: + +1. Add `install/lib` to your `CMAKE_PREFIX_PATH` (replace `install` by the install + location specified during build). +2. Use `uibase` in your `CMakeLists.txt`: + +```cmake +find_package(mo2-uibase CONFIG REQUIRED) + +add_library(myplugin SHARED) + +target_link_libraries(myplugin PRIVATE mo2::uibase) +``` diff --git a/libs/uibase/cmake/config.cmake.in b/libs/uibase/cmake/config.cmake.in new file mode 100644 index 0000000..d6d7899 --- /dev/null +++ b/libs/uibase/cmake/config.cmake.in @@ -0,0 +1,13 @@ +@PACKAGE_INIT@ + +set(_UIBASE_PREFIX_DIR ${PACKAGE_PREFIX_DIR}) + +find_package(Qt6 CONFIG REQUIRED COMPONENTS Network QuickWidgets Widgets) + +include ( "${CMAKE_CURRENT_LIST_DIR}/mo2-uibase-targets.cmake" ) + + +if (MO2_CMAKE_DEPRECATED_UIBASE_INCLUDE) + target_include_directories(mo2::uibase INTERFACE + ${_UIBASE_PREFIX_DIR}/include/uibase ${_UIBASE_PREFIX_DIR}/include/uibase/game_features) +endif() diff --git a/libs/uibase/include/uibase/delayedfilewriter.h b/libs/uibase/include/uibase/delayedfilewriter.h new file mode 100644 index 0000000..ecdfd8d --- /dev/null +++ b/libs/uibase/include/uibase/delayedfilewriter.h @@ -0,0 +1,74 @@ +#ifndef DELAYEDFILEWRITER_H +#define DELAYEDFILEWRITER_H + +#include "dllimport.h" +#include +#include +#include + +namespace MOBase +{ + +/** + * The purpose of this class is to aggregate changes to a file before writing it out + */ +class QDLLEXPORT DelayedFileWriterBase : public QObject +{ + + Q_OBJECT + +public: + /** + * @brief constructor + * @param fileName + * @param delay delay (in milliseconds) before we call the actual write function + */ + DelayedFileWriterBase(int delay = 200); + ~DelayedFileWriterBase(); + +public slots: + /** + * @brief write with delay + */ + void write(); + + /** + * @brief cancel a scheduled write (does nothing if no write is scheduled) + */ + void cancel(); + + /** + * @brief write immediately without waiting for the timer to expire + * @param ifOnTimer only write if the timer is running + */ + void writeImmediately(bool ifOnTimer); + +private slots: + void timerExpired(); + +private: + virtual void doWrite() = 0; + +private: + int m_TimerDelay; + QTimer m_Timer; +}; + +class QDLLEXPORT DelayedFileWriter : public DelayedFileWriterBase +{ +public: + typedef std::function WriterFunc; + +public: + DelayedFileWriter(WriterFunc func, int delay = 200); + +private: + void doWrite(); + +private: + WriterFunc m_Func; +}; + +} // namespace MOBase + +#endif // DELAYEDFILEWRITER_H diff --git a/libs/uibase/include/uibase/diagnosisreport.h b/libs/uibase/include/uibase/diagnosisreport.h new file mode 100644 index 0000000..4b541ff --- /dev/null +++ b/libs/uibase/include/uibase/diagnosisreport.h @@ -0,0 +1,51 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 DIAGNOSISREPORT_H +#define DIAGNOSISREPORT_H + +#include + +namespace MOBase +{ + +/** + * @brief report for a single problem reported by a plugin + */ +struct ProblemReport +{ + QString key; // a plugin-defined unique key for the issue. This is used to refer to + // the problem + enum + { + SEVERITY_REPORT, // the issue should be reported but nothing more + SEVERITY_BREAKPLUGIN, // the issue breaks the plugin (the plugin has to disable + // itself) + SEVERITY_BREAKGAME // the issue will (likely) break the game. The user will be + // warned about this every time he tries to start + } severity; + bool guidedFix; // if true, the plugin provides a guide to fixing the issue + QString shortDescription; // short description text for the overview + QString longDescription; // +}; + +} // namespace MOBase + +#endif // DIAGNOSISREPORT_H diff --git a/libs/uibase/include/uibase/dllimport.h b/libs/uibase/include/uibase/dllimport.h new file mode 100644 index 0000000..577829d --- /dev/null +++ b/libs/uibase/include/uibase/dllimport.h @@ -0,0 +1,46 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 DLLIMPORT_H +#define DLLIMPORT_H + +namespace MOBase +{ + +#ifdef _WIN32 + #if defined(UIBASE_EXPORT) + #define QDLLEXPORT __declspec(dllexport) + #elif defined(_NODLL) + #define QDLLEXPORT + #else + #undef DLLEXPORT + #define QDLLEXPORT __declspec(dllimport) + #endif +#else + #if defined(UIBASE_EXPORT) + #define QDLLEXPORT __attribute__((visibility("default"))) + #else + #define QDLLEXPORT + #endif +#endif + +} // namespace MOBase + +#endif // DLLIMPORT_H diff --git a/libs/uibase/include/uibase/errorcodes.h b/libs/uibase/include/uibase/errorcodes.h new file mode 100644 index 0000000..e880ce0 --- /dev/null +++ b/libs/uibase/include/uibase/errorcodes.h @@ -0,0 +1,21 @@ +#ifndef UIBASE_ERRORCODES_H +#define UIBASE_ERRORCODES_H + +#include "dllimport.h" +#include + +#ifdef _WIN32 +#include +#else +// POSIX: use uint32_t as DWORD equivalent +using DWORD = uint32_t; +#endif + +namespace MOBase +{ + +QDLLEXPORT const wchar_t* errorCodeName(DWORD code); + +} // namespace MOBase + +#endif // UIBASE_ERRORCODES_H diff --git a/libs/uibase/include/uibase/eventfilter.h b/libs/uibase/include/uibase/eventfilter.h new file mode 100644 index 0000000..3878a50 --- /dev/null +++ b/libs/uibase/include/uibase/eventfilter.h @@ -0,0 +1,45 @@ +/* +Copyright (C) 2016 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#pragma once + +#include "dllimport.h" +#include +#include + +namespace MOBase +{ + +class QDLLEXPORT EventFilter : public QObject +{ + + Q_OBJECT + + typedef std::function HandlerFunc; + +public: + EventFilter(QObject* parent, const HandlerFunc& handler); + + virtual bool eventFilter(QObject* obj, QEvent* event) override; + +private: + HandlerFunc m_Handler; +}; + +} // namespace MOBase diff --git a/libs/uibase/include/uibase/exceptions.h b/libs/uibase/include/uibase/exceptions.h new file mode 100644 index 0000000..58b0d34 --- /dev/null +++ b/libs/uibase/include/uibase/exceptions.h @@ -0,0 +1,58 @@ +#ifndef UIBASE_EXCEPTIONS_H +#define UIBASE_EXCEPTIONS_H + +#include + +#include +#include + +#include "dllimport.h" + +namespace MOBase +{ + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4275) // non-dll interface +#endif + +/** + * @brief exception class that takes a QString as the parameter + **/ +class QDLLEXPORT Exception : public std::exception +{ +public: + Exception(const QString& text) : m_Message(text.toUtf8()) {} + + virtual const char* what() const noexcept override { return m_Message.constData(); } + +private: + QByteArray m_Message; +}; + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +// Exception thrown in case of incompatibilities, i.e. between plugins. +class QDLLEXPORT IncompatibilityException : public Exception +{ +public: + using Exception::Exception; +}; + +// Exception thrown for invalid NXM links. +class QDLLEXPORT InvalidNXMLinkException : public Exception +{ +public: + InvalidNXMLinkException(const QString& link) + : Exception(QObject::tr("invalid nxm-link: %1").arg(link)) + {} +}; + +// alias for backward-compatibility, should be removed when possible +using MyException = Exception; + +} // namespace MOBase + +#endif diff --git a/libs/uibase/include/uibase/executableinfo.h b/libs/uibase/include/uibase/executableinfo.h new file mode 100644 index 0000000..1fe52b3 --- /dev/null +++ b/libs/uibase/include/uibase/executableinfo.h @@ -0,0 +1,67 @@ +#ifndef EXECUTABLEINFO_H +#define EXECUTABLEINFO_H + +#include "dllimport.h" +#include +#include +#include +#include + +namespace MOBase +{ + +class QDLLEXPORT ExecutableForcedLoadSetting +{ +public: + ExecutableForcedLoadSetting(const QString& process, const QString& library); + + ExecutableForcedLoadSetting& withForced(bool forced = true); + + ExecutableForcedLoadSetting& withEnabled(bool enabled = true); + + bool enabled() const; + bool forced() const; + QString library() const; + QString process() const; + +private: + bool m_Enabled; + QString m_Process; + QString m_Library; + bool m_Forced; +}; + +class QDLLEXPORT ExecutableInfo +{ +public: + ExecutableInfo(const QString& title, const QFileInfo& binary); + + ExecutableInfo& withArgument(const QString& argument); + + ExecutableInfo& withWorkingDirectory(const QDir& workingDirectory); + + ExecutableInfo& withSteamAppId(const QString& appId); + + ExecutableInfo& asCustom(); + + bool isValid() const; + + QString title() const; + QFileInfo binary() const; + QStringList arguments() const; + QDir workingDirectory() const; + QString steamAppID() const; + bool isCustom() const; + +private: + QString m_Title; + QFileInfo m_Binary; + QStringList m_Arguments; + QDir m_WorkingDirectory; + QString m_SteamAppID; + bool m_Custom{false}; +}; + +} // namespace MOBase + +#endif // EXECUTABLEINFO_H diff --git a/libs/uibase/include/uibase/expanderwidget.h b/libs/uibase/include/uibase/expanderwidget.h new file mode 100644 index 0000000..0a5b07f --- /dev/null +++ b/libs/uibase/include/uibase/expanderwidget.h @@ -0,0 +1,63 @@ +#ifndef EXPANDERWIDGET_H +#define EXPANDERWIDGET_H + +#include "dllimport.h" +#include + +namespace MOBase +{ + +/* Takes a QToolButton and a widget and creates an expandable widget. + **/ +class QDLLEXPORT ExpanderWidget : public QObject +{ + Q_OBJECT; + +public: + /** empty expander, use set() + **/ + ExpanderWidget(); + + /** see set() + **/ + ExpanderWidget(QToolButton* button, QWidget* content); + + /** @brief sets the button and content widgets to use + * the button will be given an arrow icon, clicking it will toggle the + * visibility of the given widget + * @param button the button that toggles the content + * @param content the widget that will be shown or hidden + * @param opened initial state, defaults to closed + **/ + void set(QToolButton* button, QWidget* content, bool opened = false); + + /** either opens or closes the expander depending on the current state + **/ + void toggle(); + + /** sets the current state of the expander + **/ + void toggle(bool b); + + /** returns whether the expander is currently opened + **/ + bool opened() const; + + QByteArray saveState() const; + void restoreState(const QByteArray& a); + + QToolButton* button() const; + +signals: + void aboutToToggle(bool b); + void toggled(bool b); + +private: + QToolButton* m_button; + QWidget* m_content; + bool opened_; +}; + +} // namespace MOBase + +#endif // EXPANDERWIDGET_H diff --git a/libs/uibase/include/uibase/filemapping.h b/libs/uibase/include/uibase/filemapping.h new file mode 100644 index 0000000..2498c10 --- /dev/null +++ b/libs/uibase/include/uibase/filemapping.h @@ -0,0 +1,17 @@ +#ifndef FILEMAPPING_H +#define FILEMAPPING_H + +#include +#include + +struct Mapping +{ + QString source; + QString destination; + bool isDirectory; + bool createTarget; +}; + +typedef std::vector MappingType; + +#endif // FILEMAPPING_H diff --git a/libs/uibase/include/uibase/filesystemutilities.h b/libs/uibase/include/uibase/filesystemutilities.h new file mode 100644 index 0000000..ae074c6 --- /dev/null +++ b/libs/uibase/include/uibase/filesystemutilities.h @@ -0,0 +1,36 @@ +#ifndef FILESYSTEMUTILIITES_H +#define FILESYSTEMUTILITIES_H + +#include + +#include "dllimport.h" + +namespace MOBase +{ +/** + * @brief fix a directory name so it can be dealt with by windows explorer + * @return false if there was no way to convert the name into a valid one + **/ +QDLLEXPORT bool fixDirectoryName(QString& name); + +/** + * @brief ensures a file name is valid + * + * @param name the file name being sanitized + * @param replacement invalid characters are replaced with this string + * @return the sanitized file name + **/ +QDLLEXPORT QString sanitizeFileName(const QString& name, + const QString& replacement = QString("")); + +/** + * @brief checks file name validity per sanitizeFileName + * + * @param name the file name being checked + * @return true if the given file name is valid + **/ +QDLLEXPORT bool validFileName(const QString& name); + +} // namespace MOBase + +#endif // FILESYSTEM_H diff --git a/libs/uibase/include/uibase/filterwidget.h b/libs/uibase/include/uibase/filterwidget.h new file mode 100644 index 0000000..b6f09cd --- /dev/null +++ b/libs/uibase/include/uibase/filterwidget.h @@ -0,0 +1,148 @@ +#ifndef FILTERWIDGET_H +#define FILTERWIDGET_H + +#include "dllimport.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MOBase +{ + +class EventFilter; +class FilterWidget; + +class QDLLEXPORT FilterWidgetProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT; + +public: + FilterWidgetProxyModel(FilterWidget& fw, QWidget* parent = nullptr); + void refreshFilter() { +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + beginFilterChange(); +#endif + invalidateFilter(); + } + +protected: + bool filterAcceptsRow(int row, const QModelIndex& parent) const override; + void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; + +private: + FilterWidget& m_filter; + + bool columnMatches(int sourceRow, const QModelIndex& sourceParent, int c, + const QRegularExpression& what) const; +}; + +class QDLLEXPORT FilterWidget : public QObject +{ + Q_OBJECT; + +public: + struct Options + { + bool useRegex = false; + bool regexCaseSensitive = false; + bool regexExtended = false; + bool scrollToSelection = false; + }; + + using predFun = std::function; + using sortFun = std::function; + + FilterWidget(); + + static void setOptions(const Options& o); + static Options options(); + + void setEdit(QLineEdit* edit); + void setList(QAbstractItemView* list); + void clear(); + void scrollToSelection(); + bool empty() const; + + void setUpdateDelay(bool b); + bool hasUpdateDelay() const; + + void setUseSourceSort(bool b); + bool useSourceSort() const; + + void setSortPredicate(sortFun f); + const sortFun& sortPredicate() const; + + void setFilterColumn(int i); + int filterColumn() const; + + void setFilteringEnabled(bool b); + bool filteringEnabled() const; + + void setFilteredBorder(bool b); + bool filteredBorder() const; + + FilterWidgetProxyModel* proxyModel(); + QAbstractItemModel* sourceModel(); + + QModelIndex mapFromSource(const QModelIndex& index) const; + QModelIndex mapToSource(const QModelIndex& index) const; + QItemSelection mapSelectionFromSource(const QItemSelection& sel) const; + QItemSelection mapSelectionToSource(const QItemSelection& sel) const; + + bool matches(predFun pred) const; + bool matches(const QString& s) const; + +signals: + void aboutToChange(const QString& oldFilter, const QString& newFilter); + void changed(const QString& oldFilter, const QString& newFilter); + +private: + using Compiled = QList>; + + QLineEdit* m_edit; + QAbstractItemView* m_list; + FilterWidgetProxyModel* m_proxy; + EventFilter* m_eventFilter; + QToolButton* m_clear; + QString m_text; + Compiled m_compiled; + QTimer* m_timer; + std::vector m_shortcuts; + bool m_useDelay; + bool m_valid; + bool m_useSourceSort; + sortFun m_lt; + int m_filterColumn; + bool m_filteringEnabled; + bool m_filteredBorder; + + void hookEdit(); + void unhookEdit(); + + void hookList(); + void setShortcuts(); + void unhookList(); + + void createClear(); + void repositionClearButton(); + + void onTextChanged(); + void onFind(); + void onReset(); + void onResized(); + void onContextMenu(QObject*, QContextMenuEvent* e); + + void set(); + void update(); + void compile(); +}; + +} // namespace MOBase + +#endif // FILTERWIDGET_H diff --git a/libs/uibase/include/uibase/finddialog.h b/libs/uibase/include/uibase/finddialog.h new file mode 100644 index 0000000..77a8ac3 --- /dev/null +++ b/libs/uibase/include/uibase/finddialog.h @@ -0,0 +1,80 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 FINDDIALOG_H +#define FINDDIALOG_H + +#include + +namespace Ui +{ +class FindDialog; +} + +namespace MOBase +{ + +/** + * @brief Find dialog used in the TextView dialog + **/ +class FindDialog : public QDialog +{ + + Q_OBJECT + +public: + /** + * @brief constructor + * + * @param parent parent widget + **/ + explicit FindDialog(QWidget* parent = 0); + + ~FindDialog(); + +signals: + + /** + * @brief emitted when the user wants to jump to the next location matching the + *pattern + **/ + void findNext(); + + /** + * @brief emitted when the user changes the pattern to search for + * + * @param pattern the new search pattern + **/ + void patternChanged(const QString& pattern); + +private slots: + void on_nextBtn_clicked(); + + void on_patternEdit_textChanged(const QString& arg1); + + void on_closeBtn_clicked(); + +private: + Ui::FindDialog* ui; +}; + +} // namespace MOBase + +#endif // FINDDIALOG_H diff --git a/libs/uibase/include/uibase/formatters.h b/libs/uibase/include/uibase/formatters.h new file mode 100644 index 0000000..e33aa86 --- /dev/null +++ b/libs/uibase/include/uibase/formatters.h @@ -0,0 +1,5 @@ +#pragma once + +#include "./formatters/enums.h" +#include "./formatters/qt.h" +#include "./formatters/strings.h" diff --git a/libs/uibase/include/uibase/formatters/enums.h b/libs/uibase/include/uibase/formatters/enums.h new file mode 100644 index 0000000..6538140 --- /dev/null +++ b/libs/uibase/include/uibase/formatters/enums.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +template + requires std::is_enum_v +struct std::formatter : std::formatter, CharT> +{ + template + FmtContext::iterator format(Enum v, FmtContext& ctx) const + { + return std::formatter, CharT>::format( + static_cast>(v), ctx); + } +}; diff --git a/libs/uibase/include/uibase/formatters/qt.h b/libs/uibase/include/uibase/formatters/qt.h new file mode 100644 index 0000000..de64aa8 --- /dev/null +++ b/libs/uibase/include/uibase/formatters/qt.h @@ -0,0 +1,87 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +template +struct std::formatter : std::formatter, CharT> +{ + template + FmtContext::iterator format(QSize s, FmtContext& ctx) const + { + return std::format_to(ctx.out(), "QSize({}, {})", s.width(), s.height()); + } +}; + +template +struct std::formatter : std::formatter, CharT> +{ + template + FmtContext::iterator format(QRect r, FmtContext& ctx) const + { + return std::format_to(ctx.out(), "QRect({},{}-{},{})", r.left(), r.top(), r.right(), + r.bottom()); + } +}; + +template +struct std::formatter : std::formatter, CharT> +{ + template + FmtContext::iterator format(QColor c, FmtContext& ctx) const + { + return std::format_to(ctx.out(), "QColor({}, {}, {}, {})", c.red(), c.green(), + c.blue(), c.alpha()); + } +}; + +template +struct std::formatter + : std::formatter, CharT> +{ + template + FmtContext::iterator format(QByteArray v, FmtContext& ctx) const + { + return std::format_to(ctx.out(), "QByteArray({} bytes)", v.size()); + } +}; + +template +struct std::formatter : std::formatter, CharT> +{ + template + FmtContext::iterator format(QVariant v, FmtContext& ctx) const + { + return std::format_to( + ctx.out(), "QVariant(type={}, value={})", v.typeName(), + (v.typeId() == QMetaType::Type::QByteArray ? "(binary)" : v.toString())); + } +}; + +template +struct std::formatter : std::formatter +{ + template + FmtContext::iterator format(QFlag v, FmtContext& ctx) const + { + return std::formatter::format(static_cast(v), ctx); + } +}; + +template +struct std::formatter, CharT> : std::formatter +{ + template + FmtContext::iterator format(QFlags v, FmtContext& ctx) const + { + // TODO: display flags has aa | bb | cc? + return std::formatter::format(v.toInt(), ctx); + } +}; diff --git a/libs/uibase/include/uibase/formatters/strings.h b/libs/uibase/include/uibase/formatters/strings.h new file mode 100644 index 0000000..bc31ef9 --- /dev/null +++ b/libs/uibase/include/uibase/formatters/strings.h @@ -0,0 +1,89 @@ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace MOBase::details +{ +template +inline std::basic_string toStdBasicString(QString const& qstring); + +template <> +inline std::basic_string toStdBasicString(QString const& qstring) +{ + return qstring.toStdString(); +} +template <> +inline std::basic_string toStdBasicString(QString const& qstring) +{ + return qstring.toStdWString(); +} +template <> +inline std::basic_string toStdBasicString(QString const& qstring) +{ + return qstring.toStdU16String(); +} +template <> +inline std::basic_string toStdBasicString(QString const& qstring) +{ + return qstring.toStdU32String(); +} + +inline QString fromStdBasicString(std::string const& value) +{ + return QString::fromStdString(value); +} +inline QString fromStdBasicString(std::wstring const& value) +{ + return QString::fromStdWString(value); +} +inline QString fromStdBasicString(std::u16string const& value) +{ + return QString::fromStdU16String(value); +} +inline QString fromStdBasicString(std::u32string const& value) +{ + return QString::fromStdU32String(value); +} +} // namespace MOBase::details + +template +struct std::formatter : std::formatter, CharT> +{ + template + FmtContext::iterator format(QString s, FmtContext& ctx) const + { + return std::formatter, CharT>::format( + MOBase::details::toStdBasicString(s), ctx); + } +}; + +template + requires(!std::is_same_v) +struct std::formatter, CharT2> + : std::formatter, CharT2> +{ + template + FmtContext::iterator format(std::basic_string s, FmtContext& ctx) const + { + return std::formatter, CharT2>::format( + MOBase::details::toStdBasicString( + MOBase::details::fromStdBasicString(s)), + ctx); + } +}; + +template +struct std::formatter : std::formatter +{ + template + FmtContext::iterator format(QStringView s, FmtContext& ctx) const + { + return std::formatter::format(s.toString(), ctx); + } +}; diff --git a/libs/uibase/include/uibase/game_features/bsainvalidation.h b/libs/uibase/include/uibase/game_features/bsainvalidation.h new file mode 100644 index 0000000..926753d --- /dev/null +++ b/libs/uibase/include/uibase/game_features/bsainvalidation.h @@ -0,0 +1,26 @@ +#ifndef UIBASE_GAMEFEATURES_BSAINVALIDATION_H +#define UIBASE_GAMEFEATURES_BSAINVALIDATION_H + +#include + +#include "./game_feature.h" + +namespace MOBase +{ +class IProfile; + +class BSAInvalidation : public details::GameFeatureCRTP +{ +public: + virtual bool isInvalidationBSA(const QString& bsaName) = 0; + + virtual void deactivate(MOBase::IProfile* profile) = 0; + + virtual void activate(MOBase::IProfile* profile) = 0; + + virtual bool prepareProfile(MOBase::IProfile* profile) = 0; +}; + +} // namespace MOBase + +#endif // BSAINVALIDATION_H diff --git a/libs/uibase/include/uibase/game_features/dataarchives.h b/libs/uibase/include/uibase/game_features/dataarchives.h new file mode 100644 index 0000000..65c1419 --- /dev/null +++ b/libs/uibase/include/uibase/game_features/dataarchives.h @@ -0,0 +1,35 @@ +#ifndef UIBASE_GAMEFEATURES_DATAARCHIVES_H +#define UIBASE_GAMEFEATURES_DATAARCHIVES_H + +#include +#include + +#include "./game_feature.h" + +namespace MOBase +{ +class IProfile; + +class DataArchives : public details::GameFeatureCRTP +{ +public: + virtual QStringList vanillaArchives() const = 0; + + virtual QStringList archives(const MOBase::IProfile* profile) const = 0; + + /** + * @brief add an archive to the archive list + * @param profile the profile for which to change the archive list + * @param index index to insert before. 0 is the beginning of the list, INT_MAX can be + * used for the end of the list + * @param archiveName the archive to add + */ + virtual void addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) = 0; + + virtual void removeArchive(MOBase::IProfile* profile, const QString& archiveName) = 0; +}; + +} // namespace MOBase + +#endif // DATAARCHIVES diff --git a/libs/uibase/include/uibase/game_features/game_feature.h b/libs/uibase/include/uibase/game_features/game_feature.h new file mode 100644 index 0000000..6e0dd4e --- /dev/null +++ b/libs/uibase/include/uibase/game_features/game_feature.h @@ -0,0 +1,40 @@ +#ifndef UIBASE_GAMEFEATURES_GAMEFEATURE_H +#define UIBASE_GAMEFEATURES_GAMEFEATURE_H + +#include + +namespace MOBase +{ + +/** + * Empty class that is inherit by all game features. + */ +class GameFeature +{ +public: + GameFeature() = default; + virtual ~GameFeature() = 0; + + /** + * @brief Retrieve the type index of the main game feature this feature extends. + */ + virtual const std::type_info& typeInfo() const = 0; +}; + +// Pure virtual destructor must still have a definition +inline GameFeature::~GameFeature() {} + +namespace details +{ + + template + class GameFeatureCRTP : public GameFeature + { + const std::type_info& typeInfo() const final { return typeid(T); } + }; + +} // namespace details + +} // namespace MOBase + +#endif diff --git a/libs/uibase/include/uibase/game_features/gameplugins.h b/libs/uibase/include/uibase/game_features/gameplugins.h new file mode 100644 index 0000000..3406d91 --- /dev/null +++ b/libs/uibase/include/uibase/game_features/gameplugins.h @@ -0,0 +1,25 @@ +#ifndef UIBASE_GAMEFEATURES_GAMEPLUGINS_H +#define UIBASE_GAMEFEATURES_GAMEPLUGINS_H + +#include + +#include "./game_feature.h" + +namespace MOBase +{ +class IPluginList; + +class GamePlugins : public details::GameFeatureCRTP +{ +public: + virtual void writePluginLists(const MOBase::IPluginList* pluginList) = 0; + virtual void readPluginLists(MOBase::IPluginList* pluginList) = 0; + virtual QStringList getLoadOrder() = 0; + virtual bool lightPluginsAreSupported() { return false; } + virtual bool mediumPluginsAreSupported() { return false; } + virtual bool blueprintPluginsAreSupported() { return false; } +}; + +} // namespace MOBase + +#endif // GAMEPLUGINS_H diff --git a/libs/uibase/include/uibase/game_features/igamefeatures.h b/libs/uibase/include/uibase/game_features/igamefeatures.h new file mode 100644 index 0000000..2d89b3c --- /dev/null +++ b/libs/uibase/include/uibase/game_features/igamefeatures.h @@ -0,0 +1,166 @@ +#ifndef UIBASE_GAMEFEATURES_IGAMEFEATURES_H +#define UIBASE_GAMEFEATURES_IGAMEFEATURES_H + +#include +#include + +#include + +#include "game_feature.h" + +namespace MOBase +{ + +class IPluginGame; + +// top-level game features +class BSAInvalidation; +class DataArchives; +class GamePlugins; +class LocalSavegames; +class ModDataChecker; +class ModDataContent; +class SaveGameInfo; +class ScriptExtender; +class UnmanagedMods; + +namespace details +{ + + // use pointers in the tuple since are only forward-declaring the features here + using BaseGameFeaturesP = + std::tuple; + +} // namespace details + +// simple concept that only restricting template function that should take a game +// feature to actually viable game feature types +// +template +concept BaseGameFeature = requires(T) { + { + std::get(std::declval()) + } -> std::convertible_to; +}; + +/** + * @brief Interface to game features. + * + */ +class IGameFeatures +{ +public: + /** + * @brief Register game feature for the specified game. + * + * This method register a game feature to combine or replace the same feature provided + * by the game. Some features are merged (e.g., ModDataContent, ModDataChecker), while + * other override previous features (e.g., SaveGameInfo). + * + * For features that can be combined, the priority argument indicates the order of + * priority (e.g., the order of the checks for ModDataChecker). For other features, + * the feature with the highest priority will be used. The features provided by the + * game plugin itself always have lowest priority. + * + * The feature is associated to the plugin that registers it, if the plugin is + * disabled, the feature will not be available. + * + * This function will return True if the feature was registered, even if the feature + * is not used du to its low priority. + * + * @param games Names of the game to enable the feature for. + * @param feature Game feature to register. + * @param priority Priority of the game feature. If the plugin registering the feature + * is a game plugin, this parameter is ignored. + * @param replace If true, remove features of the same kind registered by the current + * plugin, otherwise add the feature alongside existing ones. + * + * @return true if the game feature was properly registered, false otherwise. + */ + virtual bool registerFeature(QStringList const& games, + std::shared_ptr feature, int priority, + bool replace = false) = 0; + + /** + * @brief Register game feature for the specified game. + * + * See first overload for more details. + * + * @param game Game to enable the feature for. + * @param feature Game feature to register. + * @param priority Priority of the game feature. + * @param replace If true, remove features of the same kind registered by the current + * plugin, otherwise add the feature alongside existing ones. + * + * @return true if the game feature was properly registered, false otherwise. + */ + virtual bool registerFeature(IPluginGame* game, std::shared_ptr feature, + int priority, bool replace = false) = 0; + + /** + * @brief Register game feature for all games. + * + * See first overload for more details. + * + * @param feature Game feature to register. + * @param priority Priority of the game feature. + * @param replace If true, remove features of the same kind registered by the current + * plugin, otherwise add the feature alongside existing ones. + * + * @return true if the game feature was properly registered, false otherwise. + */ + virtual bool registerFeature(std::shared_ptr feature, int priority, + bool replace = false) = 0; + + /** + * @brief Unregister the given game feature. + * + * This function is safe to use even if the feature was not registered before. + * + * @param feature Feature to unregister. + */ + virtual bool unregisterFeature(std::shared_ptr feature) = 0; + + /** + * @brief Unregister all features of the given type registered by the calling plugin. + * + * This function is safe to use even if the plugin has no feature of the given type + * register. + * + * @return the number of features unregistered. + * + * @tparam Feature Type of game feature to remove. + */ + template + int unregisterFeatures() + { + return unregisterFeaturesImpl(typeid(Feature)); + } + + /** + * Retrieve the given game feature, if one exists. + * + * @return the feature of the given type, if one exists, otherwise a null pointer. + */ + template + std::shared_ptr gameFeature() const + { + // gameFeatureImpl ensure that the returned pointer is of the right type (or + // nullptr), so reinterpret_cast should be fine here + return std::dynamic_pointer_cast(gameFeatureImpl(typeid(T))); + } + +public: + virtual ~IGameFeatures() = default; + +protected: + virtual std::shared_ptr + gameFeatureImpl(std::type_info const& info) const = 0; + virtual int unregisterFeaturesImpl(std::type_info const& info) = 0; +}; + +} // namespace MOBase + +#endif diff --git a/libs/uibase/include/uibase/game_features/localsavegames.h b/libs/uibase/include/uibase/game_features/localsavegames.h new file mode 100644 index 0000000..f425431 --- /dev/null +++ b/libs/uibase/include/uibase/game_features/localsavegames.h @@ -0,0 +1,21 @@ +#ifndef UIBASE_GAMEFEATURES_LOCALSAVEGAMES_H +#define UIBASE_GAMEFEATURES_LOCALSAVEGAMES_H + +#include + +#include "../filemapping.h" +#include "./game_feature.h" + +namespace MOBase +{ +class IProfile; + +class LocalSavegames : public details::GameFeatureCRTP +{ +public: + virtual MappingType mappings(const QDir& profileSaveDir) const = 0; + virtual bool prepareProfile(MOBase::IProfile* profile) = 0; +}; +} // namespace MOBase + +#endif // LOCALSAVEGAMES_H diff --git a/libs/uibase/include/uibase/game_features/moddatachecker.h b/libs/uibase/include/uibase/game_features/moddatachecker.h new file mode 100644 index 0000000..4c95f79 --- /dev/null +++ b/libs/uibase/include/uibase/game_features/moddatachecker.h @@ -0,0 +1,66 @@ +#ifndef UIBASE_GAMEFEATURES_MODDATACHECKER_H +#define UIBASE_GAMEFEATURES_MODDATACHECKER_H + +#include + +#include "./game_feature.h" + +namespace MOBase +{ +class IFileTree; + +class ModDataChecker : public details::GameFeatureCRTP +{ +public: + /** + * + */ + enum class CheckReturn + { + INVALID, + FIXABLE, + VALID + }; + + /** + * @brief Check that the given filetree represent a valid mod layout, or can be easily + * fixed. + * + * This method is mainly used during installation (to find which installer should + * be used or to recurse into multi-level archives), or to quickly indicates to a + * user if a mod looks valid. + * + * This method does not have to be exact, it only has to indicate if the given tree + * looks like a valid mod or not by quickly checking the structure (heavy operations + * should be avoided). + * + * If the tree can be fixed by the `fix()` method, this method should return + * `FIXABLE`. `FIXABLE` should only be returned when it is guaranteed that `fix()` can + * fix the tree. + * + * @param tree The tree starting at the root of the "data" folder. + * + * @return whether the tree is invalid, fixable or valid. + */ + virtual CheckReturn + dataLooksValid(std::shared_ptr fileTree) const = 0; + + /** + * @brief Try to fix the given tree. + * + * This method is used during installation to try to fix invalid archives and will + * only be called if dataLooksValid returned `FIXABLE`. + * + * @param tree The tree to try to fix. Can be modified during the process. + * + * @return the fixed tree, or a null pointer if the tree could not be fixed. + */ + virtual std::shared_ptr + fix(std::shared_ptr fileTree) const + { + return nullptr; + } +}; +} // namespace MOBase + +#endif diff --git a/libs/uibase/include/uibase/game_features/moddatacontent.h b/libs/uibase/include/uibase/game_features/moddatacontent.h new file mode 100644 index 0000000..b86527c --- /dev/null +++ b/libs/uibase/include/uibase/game_features/moddatacontent.h @@ -0,0 +1,122 @@ +#ifndef UIBASE_GAMEFEATURES_MODDATACONTENT_H +#define UIBASE_GAMEFEATURES_MODDATACONTENT_H + +#include +#include +#include + +#include + +#include "./game_feature.h" + +namespace MOBase +{ +class IFileTree; + +/** + * The ModDataContent feature is used (when available) to indicate to users the content + * of mods in the "Content" column. + * + * The feature exposes a list of possible content types, each associated with an ID, a + * name and an icon. The icon is the path to either: + * - A Qt resource or; + * - A file on the disk. + * + * In order to facilitate the implementation, MO2 already provides a set of icons that + * can be used. Those icons are all under :/MO/gui/content (e.g. :/MO/gui/content/plugin + * or :/MO/gui/content/music). + * + * The list of available icons is: + * - plugin: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/jigsaw-piece.png + * - skyproc: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/hand-of-god.png + * - texture: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/empty-chessboard.png + * - music: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/double-quaver.png + * - sound: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/lyre.png + * - interface: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/usable.png + * - skse: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/checkbox-tree.png + * - script: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/tinker.png + * - mesh: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/breastplate.png + * - string: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/conversation.png + * - bsa: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/locked-chest.png + * - menu: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/config.png + * - inifile: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/feather-and-scroll.png + * - modgroup: + * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/xedit.png + */ +class ModDataContent : public details::GameFeatureCRTP +{ +public: + struct Content + { + + /** + * @param id ID of this content. + * @param name Name of this content. + * @param icon Path to the icon for this content. Can be either a path + * to an image on the disk, or to a resource. Can be an empty string if + * filterOnly is true. + * @param filterOnly Indicates if the content should only be show in the filter + * criteria and not in the actual Content column. + */ + Content(int id, QString name, QString icon, bool filterOnly = false) + : m_Id{id}, m_Name{name}, m_Icon{icon}, m_FilterOnly{filterOnly} + {} + + /** + * @return the ID of this content. + */ + int id() const { return m_Id; } + + /** + * @return the name of this content. + */ + QString name() const { return m_Name; } + + /** + * @return the path to the icon of this content (can be a Qt resource path). + */ + QString icon() const { return m_Icon; } + + /** + * @return true if this content is only meant to be used as a filter criteria. + */ + bool isOnlyForFilter() const { return m_FilterOnly; } + + private: + int m_Id; + QString m_Name; + QString m_Icon; + bool m_FilterOnly; + }; + + /** + * @return the list of all possible contents for the corresponding game. + */ + virtual std::vector getAllContents() const = 0; + + /** + * @brief Retrieve the list of contents in the given tree. + * + * @param fileTree The tree corresponding to the mod to retrieve contents for. + * + * @return the IDs of the content in the given tree. + */ + virtual std::vector + getContentsFor(std::shared_ptr fileTree) const = 0; +}; +} // namespace MOBase + +#endif diff --git a/libs/uibase/include/uibase/game_features/savegameinfo.h b/libs/uibase/include/uibase/game_features/savegameinfo.h new file mode 100644 index 0000000..b5ea216 --- /dev/null +++ b/libs/uibase/include/uibase/game_features/savegameinfo.h @@ -0,0 +1,52 @@ +#ifndef UIBASE_GAMEFEATURES_SAVEGAMEINFO_H +#define UIBASE_GAMEFEATURES_SAVEGAMEINFO_H + +#include +#include +#include +#include + +#include "./game_feature.h" + +namespace MOBase +{ +class ISaveGame; +class ISaveGameInfoWidget; + +/** Feature to get hold of stuff to do with save games */ +class SaveGameInfo : public details::GameFeatureCRTP +{ +public: + virtual ~SaveGameInfo() {} + + typedef QStringList ProvidingModules; + typedef QMap MissingAssets; + + /** + * @brief Get missing items from a save. + * + * @param save The save to retrieve missing assets from. + * + * @returns a collection of missing assets and the modules that can supply those + * assets. + * + * Note that in the situation where 'module' and 'asset' are indistinguishable, + * both still have to be supplied. + */ + virtual MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const = 0; + + /** + * @brief Get a widget to display over the save game list. + * + * @param parent The parent widget. + * + * @returns a Qt widget to display saves Widget. + * + * It is permitted to return a null pointer to indicate the plugin does not have a + * nice visual way of displaying save game contents. + */ + virtual MOBase::ISaveGameInfoWidget* getSaveGameWidget(QWidget* parent = 0) const = 0; +}; +} // namespace MOBase + +#endif // SAVEGAMEINFO_H diff --git a/libs/uibase/include/uibase/game_features/scriptextender.h b/libs/uibase/include/uibase/game_features/scriptextender.h new file mode 100644 index 0000000..d6028ae --- /dev/null +++ b/libs/uibase/include/uibase/game_features/scriptextender.h @@ -0,0 +1,53 @@ +#ifndef UIBASE_GAMEFEATURES_SCRIPTEXTENDER +#define UIBASE_GAMEFEATURES_SCRIPTEXTENDER + +#include + +#ifdef _WIN32 +#include +#else +using WORD = uint16_t; +#endif + +#include +#include + +#include "./game_feature.h" + +namespace MOBase +{ + +class ScriptExtender : public details::GameFeatureCRTP +{ + +public: + virtual ~ScriptExtender() {} + + /** Get the name of the script extender binary */ + virtual QString BinaryName() const = 0; + + /** Get the script extender plugin path*/ + virtual QString PluginPath() const = 0; + + /** The loader to use to ensure the game runs with the script extender */ + virtual QString loaderName() const = 0; + + /** Full path of the loader */ + virtual QString loaderPath() const = 0; + + /** Extension of the script extender save game */ + virtual QString savegameExtension() const = 0; + + /** Returns true if the extender is installed */ + virtual bool isInstalled() const = 0; + + /** Get version of extender */ + virtual QString getExtenderVersion() const = 0; + + /** Get CPU platform of extender */ + virtual WORD getArch() const = 0; +}; + +} // namespace MOBase + +#endif // SCRIPTEXTENDER diff --git a/libs/uibase/include/uibase/game_features/unmanagedmods.h b/libs/uibase/include/uibase/game_features/unmanagedmods.h new file mode 100644 index 0000000..6eeb8a7 --- /dev/null +++ b/libs/uibase/include/uibase/game_features/unmanagedmods.h @@ -0,0 +1,45 @@ +#ifndef UIBASE_GAMEFEATURES_UNMANAGEDMODS_H +#define UIBASE_GAMEFEATURES_UNMANAGEDMODS_H + +#include +#include +#include + +#include "./game_feature.h" + +namespace MOBase +{ + +class UnmanagedMods : public details::GameFeatureCRTP +{ +public: + virtual ~UnmanagedMods() {} + + /** + * @param onlyOfficial if set, only official mods (dlcs) are returned + * @return the list of unmanaged mods (internal names) + */ + virtual QStringList mods(bool onlyOfficial) const = 0; + + /** + * @param modName (internal) name of the mod being requested + * @return display name of the mod + */ + virtual QString displayName(const QString& modName) const = 0; + + /** + * @param modName name of the mod being requested + * @return reference file info + */ + virtual QFileInfo referenceFile(const QString& modName) const = 0; + + /** + * @param modName name of the mod being requested + * @return list of file names (absolute paths) + */ + virtual QStringList secondaryFiles(const QString& modName) const = 0; +}; + +} // namespace MOBase + +#endif // UNMANAGEDMODS_H diff --git a/libs/uibase/include/uibase/guessedvalue.h b/libs/uibase/include/uibase/guessedvalue.h new file mode 100644 index 0000000..9c0be51 --- /dev/null +++ b/libs/uibase/include/uibase/guessedvalue.h @@ -0,0 +1,252 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 GUESSEDVALUE_H +#define GUESSEDVALUE_H + +#include +#include + +namespace MOBase +{ + +/** + * @brief describes how good the code considers a guess (i.e. for a mod name) + * this is used to determine if a name from another source should overwrite or + * not + */ +enum EGuessQuality +{ + GUESS_INVALID, /// no valid value has been set yet + GUESS_FALLBACK, /// the guess is very basic and should only be used if no other + /// source is available + GUESS_GOOD, /// considered a good guess + GUESS_META, /// the value comes from meta data and is usually what the author + /// intended + GUESS_PRESET, /// the value comes from a previous install of the same data and + /// usually represents what the user chose before + GUESS_USER /// the user selection. Always overrules other sources +}; + +/** + * Represents a value that may be set from different places. Each time the value is + * changed a "quality" is specified to say how probable it is the value is the best + * choice. Only the best choice should be used in the end but alternatives can be + * queried. This class also allows a filter to be set. If a "guess" doesn't pass the + * filter, it is ignored. + */ +template +class GuessedValue +{ +public: +public: + /** + * @brief default constructor + */ + GuessedValue(); + + /** + * @brief constructor with initial value + * + * @param reference the initial value to set + * @param quality quality of the guess + */ + GuessedValue(const T& reference, EGuessQuality quality = GUESS_USER); + + /** + * @brief + * + * @param reference + * @return GuessedValue + */ + GuessedValue& operator=(const GuessedValue& reference); + + /** + * install a filter function. This filter is applied on every update and can + * refuse the update altogether or modify the value. + * @param filterFunction the filter to apply + */ + /** + * @brief + * + * @param filterFunction + */ + void setFilter(std::function filterFunction); + + /** + * @brief + * + * @return operator const T + */ + operator const T&() const { return m_Value; } + + /** + * @brief + * + * @return T *operator -> + */ + T* operator->() { return &m_Value; } + /** + * @brief + * + * @return const T *operator -> + */ + /** + * @brief + * + * @return const T *operator -> + */ + const T* operator->() const { return &m_Value; } + + /** + * @brief + * + * @param value + * @return GuessedValue + */ + GuessedValue& update(const T& value); + /** + * @brief + * + * @param value + * @param quality + * @return GuessedValue + */ + GuessedValue& update(const T& value, EGuessQuality quality); + + /** + * @brief + * + * @return const std::set + */ + const std::set& variants() const { return m_Variants; } + +private: + T m_Value; /**< TODO */ + std::set m_Variants; /**< TODO */ + EGuessQuality m_Quality; /**< TODO */ + std::function m_Filter; +}; + +template +/** + * @brief + * + * @param + * @return bool + */ +bool nullFilter(T&) +{ + return true; +} + +/** + * @brief + * + */ +template +GuessedValue::GuessedValue() + : m_Value(), m_Quality(GUESS_INVALID), m_Filter(nullFilter) +{} + +/** + * @brief + * + * @param reference + * @param quality + */ +template +GuessedValue::GuessedValue(const T& reference, EGuessQuality quality) + : m_Value(reference), m_Variants{reference}, m_Quality(quality), + m_Filter(nullFilter) +{} + +/** + * @brief + * + * @param reference + * @return GuessedValue &GuessedValue + */ +template +GuessedValue& GuessedValue::operator=(const GuessedValue& reference) +{ + if (this != &reference) { + if (reference.m_Quality >= m_Quality) { + m_Value = reference.m_Value; + m_Quality = reference.m_Quality; + m_Filter = reference.m_Filter; + m_Variants = reference.m_Variants; + } + } + return *this; +} + +/** + * @brief + * + * @param filterFunction + */ +template +void GuessedValue::setFilter(std::function filterFunction) +{ + m_Filter = filterFunction; +} + +/** + * @brief + * + * @param value + * @return GuessedValue &GuessedValue + */ +template +GuessedValue& GuessedValue::update(const T& value) +{ + T temp = value; + if (m_Filter(temp)) { + m_Variants.insert(temp); + m_Value = temp; + } + return *this; +} + +/** + * @brief + * + * @param value + * @param quality + * @return GuessedValue &GuessedValue + */ +template +GuessedValue& GuessedValue::update(const T& value, EGuessQuality quality) +{ + T temp = value; + if (m_Filter(temp)) { + m_Variants.insert(temp); + if (quality >= m_Quality) { + m_Value = temp; + m_Quality = quality; + } + } + return *this; +} + +} // namespace MOBase + +#endif // GUESSEDVALUE_H diff --git a/libs/uibase/include/uibase/idownloadmanager.h b/libs/uibase/include/uibase/idownloadmanager.h new file mode 100644 index 0000000..f22dd0e --- /dev/null +++ b/libs/uibase/include/uibase/idownloadmanager.h @@ -0,0 +1,102 @@ +#ifndef IDOWNLOADMANAGER_H +#define IDOWNLOADMANAGER_H + +#include "dllimport.h" +#include +#include +#include +#include + +namespace MOBase +{ + +class QDLLEXPORT IDownloadManager : public QObject +{ + Q_OBJECT + +public: + IDownloadManager(QObject* parent = nullptr) : QObject(parent) {} + + /** + * @brief download a file by url. The list can contain alternative URLs to allow the + * download manager to switch in case of download problems + * @param urls list of urls to download from + * @return an id by which the download will be identified + */ + virtual int startDownloadURLs(const QStringList& urls) = 0; + + /** + * @brief download a file from www.nexusmods.com/. is always the game + * currently being managed + * @param modID id of the mod for which to download a file + * @param fileID id of the file to download + * @return an id by which the download will be identified + */ + virtual int startDownloadNexusFile(int modID, int fileID) = 0; + + /** + * @brief download a file from www.nexusmods.com/. + * @param gameName 'short' name of the game the mod is for + * @param modID id of the mod for which to download a file + * @param fileID id of the file to download + * @return an id by which the download will be identified + */ + virtual int startDownloadNexusFileForGame(const QString& gameName, int modID, + int fileID) = 0; + + /** + * @brief get the (absolute) file path of the specified download. + * @param id id of the download as returned by the download... functions + * @return absoute path to the downloaded file. This file may not yet exist if the + * download is incomplete + */ + virtual QString downloadPath(int id) = 0; + + /** + * @brief Installs a handler to be called when a download complete. + * + * @param callback The function to be called when a download complete. The argument is + * the download ID. + * + * @return true if the handler was successfully installed (there is as of now no known + * reason this should fail). + */ + virtual bool onDownloadComplete(const std::function& callback) = 0; + + /** + * @brief Installs a handler to be called when a download is paused. + * + * @param callback The function to be called when a download is paused. The argument + * is the download ID. + * + * @return true if the handler was successfully installed (there is as of now no known + * reason this should fail). + */ + virtual bool onDownloadPaused(const std::function& callback) = 0; + + /** + * @brief Installs a handler to be called when a download fails. + * + * @param callback The function to be called when a download fails. The argument is + * the download ID. + * + * @return true if the handler was successfully installed (there is as of now no known + * reason this should fail). + */ + virtual bool onDownloadFailed(const std::function& callback) = 0; + + /** + * @brief Installs a handler to be called when a download is removed. + * + * @param callback The function to be called when a download is removed. The argument + * is the download ID (which is no longer valid). + * + * @return true if the handler was successfully installed (there is as of now no known + * reason this should fail). + */ + virtual bool onDownloadRemoved(const std::function& callback) = 0; +}; + +} // namespace MOBase + +#endif // IDOWNLOADMANAGER_H diff --git a/libs/uibase/include/uibase/iexecutable.h b/libs/uibase/include/uibase/iexecutable.h new file mode 100644 index 0000000..7873b10 --- /dev/null +++ b/libs/uibase/include/uibase/iexecutable.h @@ -0,0 +1,84 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IEXECUTABLE_H +#define IEXECUTABLE_H + +#include +#include + +namespace MOBase +{ +class IExecutable +{ +public: // Information found in ExecutableInfo + /** + * @return the title of the executable. + */ + virtual const QString& title() const = 0; + + /** + * @return the file info of the executable binary. + */ + virtual const QFileInfo& binaryInfo() const = 0; + + /** + * @return the arguments to be passed to the executable. + * + * @note This API might be changed in the future to return a QStringList instead. + */ + virtual const QString& arguments() const = 0; + + /** + * @return the Steam App ID associated with this executable, or an empty string if + * there is none. + */ + virtual const QString& steamAppID() const = 0; + + /** + * @return the working directory for the executable. + */ + virtual const QString& workingDirectory() const = 0; + +public: // Information found in flags + /** + * @return true if the executable is shown on the toolbar. + */ + virtual bool isShownOnToolbar() const = 0; + + /** + * @return true if the executable's application icon is used for desktop shortcuts. + */ + virtual bool usesOwnIcon() const = 0; + + /** + * @return true if Mod Organizer should minimize to the system tray while this + * executable is running. + */ + virtual bool minimizeToSystemTray() const = 0; + + /** + * @return true if this executable is hidden in the user interface. + */ + virtual bool hide() const = 0; +}; +} // namespace MOBase + +#endif // IEXECUTABLE_H diff --git a/libs/uibase/include/uibase/iexecutableslist.h b/libs/uibase/include/uibase/iexecutableslist.h new file mode 100644 index 0000000..f8f7a3f --- /dev/null +++ b/libs/uibase/include/uibase/iexecutableslist.h @@ -0,0 +1,75 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IEXECUTABLESLIST_H +#define IEXECUTABLESLIST_H + +#include +#include + +#include + +#include "iexecutable.h" + +namespace MOBase +{ +/** + * @brief Interface to the list of executables configured in Mod Organizer. + */ +class IExecutablesList +{ +public: + /** + * @brief Retrieve all configured executables. + * + * @return a generator yielding all configured executables. + */ + virtual std::generator executables() const = 0; + + /** + * @brief Retrieve an executable by its title. + * + * @param title Title of the executable to retrieve. + * + * @return the executable with the specified title, or nullptr if not found. + */ + virtual const IExecutable* getByTitle(const QString& title) const = 0; + + /** + * @brief Retrieve an executable by its binary file info. + * + * @param info File info of the executable binary to retrieve. + * + * @return the executable with the specified binary, or nullptr if not found. + */ + virtual const IExecutable* getByBinary(const QFileInfo& info) const = 0; + + /** + * @brief Check if an executable with the specified title exists. + * + * @param title Title of the executable to check. + * + * @return true if an executable with the specified title exists, false otherwise. + */ + virtual bool contains(const QString& title) const = 0; +}; +} // namespace MOBase + +#endif // IEXECUTABLESLIST_H diff --git a/libs/uibase/include/uibase/ifiletree.h b/libs/uibase/include/uibase/ifiletree.h new file mode 100644 index 0000000..65bc171 --- /dev/null +++ b/libs/uibase/include/uibase/ifiletree.h @@ -0,0 +1,1130 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2020 MO2 Team. 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 IFILETREE_H +#define IFILETREE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "dllimport.h" +#include "utility.h" + +/** + * This header contains definition for the interface IFileTree and the FileTreeEntry + * class. + * + * The purpose of IFileTree is to expose a file tree in a user-friendly way. The + * IFileTree interface represent a "virtual" file tree: the tree may not exists on + * the disk or anywhere, it is just an abstract structure. The source of the tree + * is irrelevant to the IFileTree user and is an implementation details. + * + * IFileTree and FileTreeEntry are very intrically linked so it is not possible to + * use FileTreeEntry for something else, and the only way to create FileTreeEntry + * is through an existing IFileTree. + * + * IFileTree expose a mutable and a strict non-mutable interfaces based on + * const-qualification of methods, but all underlying implementation are obviously + * non-const. + * + * The IFileTree interface is implemented such that creating implementations should + * be fairly easy (two short methods to implement). Implementation can override other + * methods in order to reflect changes from the tree to the actual source (e.g., + * override the beforeX() methods to actually rename or move file on the disk). + * + */ + +namespace MOBase +{ + +/** + * + */ +class IFileTree; + +/** + * @brief Simple valid C++ comparator for QString that compare them case-insensitive, + * mostly useful to compare filenames on Windows. + */ +struct FileNameComparator +{ + + /** + * @brief The case sensitivity of filenames. + */ + static constexpr auto CaseSensitivity = Qt::CaseInsensitive; + + /** + * @brief Compare the two given filenames. + * + * @param lhs, rhs Filenames to compare. + * + * @return -1, 0 or 1 if the first one is less, equal or greater than the second one. + */ + static int compare(QString const& lhs, QString const& rhs) + { + return lhs.compare(rhs, CaseSensitivity); + } + + /** + * + */ + bool operator()(QString const& a, QString const& b) const + { + return compare(a, b) < 0; + } +}; + +/** + * @brief Exception thrown when an operation on the tree is not supported by the + * implementation or makes no sense (e.g., creation of a file in an archive). + */ +struct QDLLEXPORT UnsupportedOperationException : public Exception +{ + using Exception::Exception; +}; + +/** + * @brief Represent an entry in a file tree, either a file or a directory. This class + * inherited by IFileTree so that operations on entry are the same for a file or + * a directory. + * + * This class provides convenience methods to query information on the file, like its + * name or the its last modification time. It also provides a convenience astree() + * method that can be used to retrieve the tree corresponding to its entry in case the + * entry represent a directory. + * + */ +class QDLLEXPORT FileTreeEntry : public std::enable_shared_from_this +{ + +public: // Enums + /** + * @brief Enumeration of the different file type. + * + */ + enum FileType + { + DIRECTORY = 0b01, + FILE = 0b10 + }; + Q_DECLARE_FLAGS(FileTypes, FileType); + + constexpr static auto FILE_OR_DIRECTORY = FileTypes{DIRECTORY, FILE}; + +public: // Deleted operators: + FileTreeEntry(FileTreeEntry const&) = delete; + FileTreeEntry(FileTreeEntry&&) = delete; + + FileTreeEntry& operator=(FileTreeEntry const&) = delete; + FileTreeEntry& operator=(FileTreeEntry&&) = delete; + +public: // Methods + /** + * @brief Check if this entry is a file. + * + * @return true if this entry is a file, false otherwize. + */ + bool isFile() const { return astree() == nullptr; } + + /** + * @brief Check if this entry is a directory. + * + * @return true if this entry is a directory, false otherwize. + */ + bool isDir() const { return astree() != nullptr; } + + /** + * @brief Convert this entry to a tree. This method returns a null pointer + * if this entry corresponds to a file. + * + * @return this entry as a tree, or a null pointer if isDir() is false. + */ + virtual std::shared_ptr astree() { return nullptr; } + + /** + * @brief Convert this entry to a tree. This method returns a null pointer + * if this entry corresponds to a file. + * + * @return this entry as a tree, or a null pointer if isDir() is false. + */ + virtual std::shared_ptr astree() const { return nullptr; } + + /** + * @brief Retrieve the type of this entry. + * + * @return the type of this entry. + */ + FileType fileType() const { return isDir() ? DIRECTORY : FILE; } + + /** + * @brief Retrieve the name of this entry. + * + * @return the name of this entry. + */ + QString name() const { return m_Name; } + + /** + * @brief Compare the name of this entry against the given string. + * + * This method only checks the name of the entry, not the full path. + * + * @param name Name to test. + * + * @return -1, 0 or 1 depending on the result of the comparison. + */ + int compare(QString name) const { return FileNameComparator::compare(m_Name, name); } + + /** + * @brief Retrieve the "last" extension of this entry. + * + * The "last" extension is everything after the last dot in the file name. + * + * @return the last extension of this entry, or an empty string if the file has no + * extension or is directory. + */ + QString suffix() const; + + /** + * @brief Check if this entry has the given suffix. + * + * @param suffix Suffix of to check. + * + * @return true if this entry is a file and has the given suffix. + */ + bool hasSuffix(QString suffix) const; + + /** + * @brief Check if this entry has one of the given suffixes. + * + * @param suffixes Suffixes of to check. + * + * @return true if this entry is a file and has the given suffix. + */ + bool hasSuffix(QStringList suffixes) const; + + /** + * @brief Retrieve the path from this entry up to the root of the tree. + * + * This method propagate up the tree so is not constant complexity as + * the full path is never stored. + * + * @param sep The type of separator to use to create the path. + * + * @return the path from this entry to the root, including the name + * of this entry. + */ + QString path(QString sep = "\\") const { return pathFrom(nullptr, sep); } + + /** + * @brief Retrieve the path from this entry to the given tree. + * + * @param tree The tree to reach, must be a parent of this entry. + * @param sep The type of separator to use to create the path. + * + * @return the path from this entry up to the given tree, including the name + * of this entry, or QString() if the given tree is not a parent of + * this one. + */ + QString pathFrom(std::shared_ptr tree, QString sep = "\\") const; + + /** + * @brief Detach this entry from its parent tree. + * + * @return true if the entry was removed correctly, false otherwize. + */ + bool detach(); + + /** + * @brief Move this entry to the given tree. + * + * @param tree The tree to move this entry to. + * + * @return true if the entry was moved correctly, false otherwize. + */ + bool moveTo(std::shared_ptr tree); + + /** + * @brief Retrieve the immediate parent tree of this entry. + * + * @return the parent tree containing this entry, or a null pointer + * if this entry is the root or the parent tree is unreachable. + */ + std::shared_ptr parent() + { + return std::const_pointer_cast( + const_cast(this)->parent()); + } + + /** + * @brief Retrieve the immediate parent tree of this entry. + * + * @return the parent tree containing this entry, or a null pointer + * if this entry is the root or the parent tree is unreachable. + */ + std::shared_ptr parent() const { return m_Parent.lock(); } + +public: // Destructor: + virtual ~FileTreeEntry() {} + +protected: // Constructors: + /** + * @brief Create a new entry corresponding. + * + * @param parent The tree containing this entry. + * @param name The name of this entry. + */ + FileTreeEntry(std::shared_ptr parent, QString name); + + /** + * @brief Creates a new orphan entry identical to this entry. + * + */ + virtual std::shared_ptr clone() const; + + /** + * @brief Creates a new FileTreeEntry corresponding to a file with the given + * parameters. + * + * The purpose of this methods is to allow child classes corresponding to tree (i.e., + * that do not inherit directly FileTreeEntry) to create FileTreeEntry. + * + * @param parent The tree containing this file. + * @param name The name of this file. + */ + static std::shared_ptr + createFileEntry(std::shared_ptr parent, QString name); + +private: + std::weak_ptr m_Parent; + + QString m_Name; + + friend class IFileTree; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(FileTreeEntry::FileTypes); + +/** + * @brief Interface to classes that provides way to visualize and alter file trees. The + * tree may not correspond to an actual file tree on the disk (e.g., inside an archive, + * from a QTree Widget, ...). + * + * This interface already implements most of the usual methods for a file tree. Child + * classes only have to implement methods to populate the tree and to create child tree + * object. + * + * Read-only operations on the tree are thread-safe, even when the tree has not been + * populated yet. + * + * In order to prevent wrong usage of the tree, implementing classes may throw + * UnsupportedOperationException if an operation is not supported. By default, all + * operations are supported, but some may not make sense in many situations. + * + * The goal of this is not reflect the change made to a IFileTree to the disk, but child + * classes may override relevant methods to do so. + * + * The tree is built upon FileTreeEntry. A given tree holds shared pointers to its + * entries while each entry holds a weak pointer to its parent, this means that the + * descending link are strong (shared pointers) but the uplink are weaks. + * + * Accessing the parent is always done by locking the weak pointer so that returned + * pointer or either null or valid. This structure implies that as long as the initial + * root lives, entry should not be destroyed, unless the entry are detached from the + * root and no shared pointers are kept. + * + * However, it is not guarantee that one can go up the tree from a single node entry. If + * the root node is destroyed, it will not be possible to go up the tree, even if we + * still have a valid shared pointer. + * + * The inheritance is made virtual to provide a way for child classes to use both a + * custom FileTreeEntry and IFileTree implementations. This has no impact on the usage + * of the interface. + * + */ +class QDLLEXPORT IFileTree : public virtual FileTreeEntry +{ +public: // Enumerations and aliases: + /** + * + */ + enum class InsertPolicy + { + FAIL_IF_EXISTS, + REPLACE, + MERGE + }; + + /** + * @brief Special constant returns by merge when the merge failed. + */ + constexpr static std::size_t MERGE_FAILED = (std::size_t)-1; + + /** + * + */ + using OverwritesType = std::map, + std::shared_ptr>; + +public: // Iterators: + /** + * The standard iterator are constant, but the pointed value are not. Since + * we are storing a vector of shared pointer to non-const object, we need this + * wrapper to create iterators to shared pointer of const-object to have proper + * immutability when IFileTree is const-qualified. + * + * Note: convert_iterator satisfies std::forward_iterator concept but not + * ForwardIterator since dereferencing does not return an lvalue. + */ + template + struct convert_iterator + { + + using reference = U; + using difference_type = typename V::difference_type; + using value_type = U; + using pointer = U; + using iterator_category = std::forward_iterator_tag; + + friend bool operator==(convert_iterator a, convert_iterator b) + { + return a.v == b.v; + } + friend bool operator!=(convert_iterator a, convert_iterator b) + { + return a.v != b.v; + } + + reference operator*() const { return U(*v); } + reference operator->() const { return U(*v); } + + convert_iterator& operator++() + { + v++; + return *this; + } + + convert_iterator operator++(int) + { + value_type value = *(*this); + (*this)++; + return *this; + } + + public: + convert_iterator() = default; + convert_iterator(convert_iterator const&) = default; + convert_iterator(convert_iterator&&) = default; + convert_iterator& operator=(convert_iterator const&) = default; + convert_iterator& operator=(convert_iterator&&) = default; + + protected: + V v; + + convert_iterator(V v) : v{v} {} + friend class IFileTree; + }; + + using value_type = std::shared_ptr; + using reference = std::shared_ptr; + using const_reference = std::shared_ptr; + + using iterator = std::vector>::const_iterator; + using const_iterator = + convert_iterator, + std::vector>::const_iterator>; + + using reverse_iterator = + std::vector>::const_reverse_iterator; + using const_reverse_iterator = convert_iterator< + std::shared_ptr, + std::vector>::const_reverse_iterator>; + +#if __cplusplus > 201703L + static_assert(std::forward_iterator); + static_assert(std::forward_iterator); + static_assert(std::forward_iterator); + static_assert(std::forward_iterator); +#endif + +public: // Access methods: + /** + * + */ + iterator begin() { return {std::cbegin(entries())}; } + const_iterator begin() const { return {std::cbegin(entries())}; } + const_iterator cbegin() const { return {std::cbegin(entries())}; } + + /** + * + */ + reverse_iterator rbegin() { return {std::crbegin(entries())}; } + const_reverse_iterator rbegin() const { return {std::crbegin(entries())}; } + const_reverse_iterator crbegin() const { return {std::crbegin(entries())}; } + + /** + * + */ + iterator end() { return {std::cend(entries())}; } + const_iterator end() const { return {std::cend(entries())}; } + const_iterator cend() const { return {std::cend(entries())}; } + + /** + * + */ + reverse_iterator rend() { return {std::crend(entries())}; } + const_reverse_iterator rend() const { return {std::crend(entries())}; } + const_reverse_iterator crend() const { return {std::crend(entries())}; } + + /** + * @brief Retrieve the number of entries in this tree. + * + * This is constant if the tree has already been populated. + * + * @return the number of entries in this tree. + */ + std::size_t size() const { return entries().size(); } + + /** + * @brief Retrieve the file entry at the given index. + * + * @param i Index of the entry to retrieve. + * + * @return the file entry at the given index. + * + * @throw std::out_of_range if the index is invalid. + */ + std::shared_ptr at(std::size_t i) + { + if (i < size()) { + return entries()[i]; + } + throw std::out_of_range("IFileTree::at"); + } + std::shared_ptr at(std::size_t i) const + { + if (i < size()) { + return entries()[i]; + } + throw std::out_of_range("IFileTree::at"); + } + + /** + * @brief Check if this tree is empty, i.e., if it contains no entries. + * + * @return true if the tree is empty, false otherwize. + */ + bool empty() const { return size() == 0; } + + /** + * @brief Check if the given entry exists. + * + * @param path Path to the entry, separated by / or \. + * @param type The type of the entry to check. + * + * @return true if the entry was found, false otherwize. + */ + bool exists(QString path, + FileTreeEntry::FileTypes type = FileTreeEntry::FILE_OR_DIRECTORY) const; + + /** + * @brief Retrieve the given entry. + * + * If an entry is found at the given path but does not match the given type, + * a null pointer is returned. + * + * @param path Path to the entry, separated by / or \. + * @param type The type of the entry to find. + * + * @return the entry if found, a null pointer otherwize. + */ + std::shared_ptr find(QString path, FileTypes type = FILE_OR_DIRECTORY); + std::shared_ptr find(QString path, + FileTypes type = FILE_OR_DIRECTORY) const; + + /** + * @brief Convenient method around find() that returns IFileTree instead of entries. + * + * @param path Path to the directory, separated by / or \. + * + * @return the directory if found, a null pointer otherwize. + */ + std::shared_ptr findDirectory(QString path) + { + auto entry = find(path, DIRECTORY); + return (entry != nullptr && entry->isDir()) ? entry->astree() : nullptr; + } + std::shared_ptr findDirectory(QString path) const + { + auto entry = find(path, DIRECTORY); + return (entry != nullptr && entry->isDir()) ? entry->astree() : nullptr; + } + + /** + * @brief Retrieve the path from this tree to the given entry. + * + * @param entry The entry to reach, must be in this tree. + * @param sep The type of separator to use to create the path. + * + * @return the path from this tree to the given entry, including the name + * of the entry, or QString() if the given entry is not in this tree. + */ + QString pathTo(std::shared_ptr entry, QString sep = "\\") const + { + return entry->pathFrom(astree(), sep); + } + +public: // Walk & Glob operations + enum class WalkReturn + { + + /** + * @brief Continue walking normally. + */ + CONTINUE, + + /** + * @brief Stop walking normally. + */ + STOP, + + /** + * @brief Skip this folder (no effect if the entry is a file). + */ + SKIP + + }; + + /** + * @brief Walk this tree, calling the given function for each entry in it. + * + * The given callback will be called with two parameters: the path from this tree to + * the given entry (with a trailing separator, not including the entry name), and the + * actual entry. The method returns a `WalkReturn` object to indicates what to do. + * + * During the walk, parent tree are guaranteed to be visited before their childrens. + * The given function is never called with the current tree. + * + * @param callback Method to call for each entry in the tree. + */ + void + walk(std::function)> + callback, + QString sep = "\\") const; + +public: // Utility functions: + /** + * @brief Create a new orphan empty tree. + * + * @param name Name of the tree. + * + * @return a new tree without any parent. + */ + std::shared_ptr createOrphanTree(QString name = "") const; + +public: // Mutable operations: + /** + * @brief Create a new file directly under this tree. + * + * This method will return a null pointer if the file already exists and if + * replaceIfExists is false. This method invalidates iterators to this tree and + * all the subtrees present in the given path. + * + * @param name Name of the file. + * @param replaceIfExists If true and an entry already exists at the given path, + * it will be replaced by a new entry. This will replace both files and + * directories. + * + * @return the entry corresponding to the create file, or a null + * pointer if the file was not created. + */ + virtual std::shared_ptr addFile(QString path, + bool replaceIfExists = false); + + /** + * @brief Create a new directory tree under this tree. + * + * This method will create missing folders in the given path and will + * not fail if the directory already exists but will fail if the given + * path contains "." or "..". + * This method invalidates iterators to this tree and all the subtrees + * present in the given path. + * + * @param path Path to the directory. + * + * @return the entry corresponding to the created directory, or a null + * pointer if the directory was not created. + */ + virtual std::shared_ptr addDirectory(QString path); + + /** + * @brief Insert the given entry in this tree, removing it from its + * previouis parent. + * + * The entry must not be this tree or a parent entry of this tree. + * + * - If the insert policy if FAIL_IF_EXISTS, the call will fail if an entry + * with the same name already exists. + * - If the policy is REPLACE, an existing entry will be replaced by the given entry. + * - If MERGE: + * - If there is no entry with the same name, the new entry is inserted. + * - If there is an entry with the same name: + * - If both entries are files, the old file is replaced by the given entry. + * - If both entries are directories, a merge is performed as if using merge(). + * - Otherwize the insertion fails (two entries with different types). + * + * This method invalidates iterator to this tree, to the parent tree of the given + * entry, and to subtrees of this tree if the insert policy is MERGE. + * + * @param entry Entry to insert. + * @param insertPolicy Policy to use on conflict. + * + * @return an iterator to the inserted tree if it was inserted or if it + * already existed, or the end iterator if insertPolicy is FAIL_IF_EXISTS + * and an entry with the same name already exists. + */ + iterator insert(std::shared_ptr entry, + InsertPolicy insertPolicy = InsertPolicy::FAIL_IF_EXISTS); + + /** + * @brief Merge the given tree with this tree, i.e., insert all entries + * of the given tree into this tree. + * + * The tree must not be this tree or a parent entry of this tree. Files present in + * both tree will be replaced by files in the given tree. The overwrites parameter can + * be used to track the replaced files. After a merge, the source tree will be + * empty but still attached to its parent. + * + * Note that the merge process makes no distinction between files and directories + * when merging: if a directory is present in this tree and a file from source + * is in conflict with it, the tree will be removed and the file inserted; if a file + * is in this tree and a directory from source is in conflict with it, the file will + * be replaced with the directory. + * + * This method invalidates iterators to this tree, all the subtrees under this tree + * present in the given path, and all the subtrees of the given source. + * + * @param source Tree to merge. + * @param overwrites If not null, can be used to create a mapping from + * overriden file to new files. + * + * @return the number of overwritten entries, or MERGE_FAILED if the merge + * failed (e.g. because the source is a parent of this tree). + */ + std::size_t merge(std::shared_ptr source, + OverwritesType* overwrites = nullptr); + + /** + * @brief Move the given entry to the given path under this tree. + * + * The entry must not be a parent tree of this tree. This method can also be used + * to rename entries. + * + * If the insert policy if FAIL_IF_EXISTS, the call will fail if an entry + * at the same location already exists. If the policy is REPLACE, an existing + * entry will be replaced. If MERGE, the entry will be merged with the existing + * one (if the entry is a file, and a file exists, the file will be replaced). + * + * This method invalidates iterator to this tree, to the parent tree of the given + * entry, and to subtrees of this tree if the insert policy is MERGE. + * + * @param entry Entry to insert. + * @param path The path to move the entry to. If the path ends with / or \, + * the entry will be inserted in the corresponding directory instead of replacing + * it. If the given path is empty (`""`), this is equivalent to `insert()`. + * @param insertPolicy Policy to use on conflict. + * + * @return true if the entry was moved correctly, false otherwize. + */ + bool move(std::shared_ptr entry, QString path = "", + InsertPolicy insertPolicy = InsertPolicy::FAIL_IF_EXISTS); + + /** + * @brief Copy the given entry to the given path under this tree. + * + * The entry must not be a parent tree of this tree. + * + * If the insert policy if FAIL_IF_EXISTS, the call will fail if an entry + * at the same location already exists. If the policy is REPLACE, an existing + * entry will be replaced. If MERGE, the entry will be merged with the existing + * one (if the entry is a file, and a file exists, the file will be replaced). + * + * This method invalidates iterator to this tree and to subtrees of this tree if + * the insert policy is MERGE. The given entry is left untouched + * + * @param entry Entry to copy. + * @param path The path to copy the entry to. If the path ends with / or \, + * the entry will be inserted in the corresponding directory instead of replacing + * it. + * @param insertPolicy Policy to use on conflict. + * + * @return the copy of the entry if it was copied correctly, a null pointer otherwise. + */ + std::shared_ptr + copy(std::shared_ptr entry, QString path = "", + InsertPolicy insertPolicy = InsertPolicy::FAIL_IF_EXISTS); + + /** + * @brief Delete the given entry. + * + * @param entry Entry to delete. The entry must belongs to this tree (and + * not to a subtree). + * + * @return an iterator following the removed entry (might be the end + * iterator if the entry was not found or was the last). + */ + iterator erase(std::shared_ptr entry); + + /** + * @brief Delete the entry with the given name. + * + * This method does not recurse into subtrees, so the entry should be + * accessible directly from this tree. + * + * @param name Name of the entry to delete. + * + * @return a pair containing an iterator following the removed entry (might + * be the end iterator if the entry was not found or was the last) and + * the removed entry (or a null pointer if the entry was not found). + */ + std::pair> erase(QString name); + + /** + * @brief Delete (detach) all the entries from this tree + * + * This method will go through the entries in this tree and stop at the first + * entry that cannot be deleted, this means that the tree can be partially cleared. + * + * @return true if all entries could be deleted, false otherwize. + */ + bool clear(); + + /** + * @brief Delete the entries with the given names from the tree. + * + * This method does not recurse into subtrees, so the entry should be + * accessible directly from this tree. This method invalidates iterators. + * + * @param names Names of the entries to delete. + * + * @return the number of deleted entry. + */ + std::size_t removeAll(QStringList names); + + /** + * @brief Delete the entries that match the given predicate from the tree. + * + * This method does not recurse into subtrees, so the entry should be + * accessible directly from this tree. This method invalidates iterators. + * + * @param predicate Predicate that should return true for entries to delete. + * + * @return the number of deleted entry. + */ + std::size_t + removeIf(std::function const& entry)> predicate); + +public: // Inherited methods: + /** + * @brief Retrieve the tree corresponding to this entry. Returns a null pointer + * if this entry corresponds to a file. + * + * @return the tree corresponding to this entry, or a null pointer if + * isDir() is false. + */ + virtual std::shared_ptr astree() override + { + return std::dynamic_pointer_cast(shared_from_this()); + } + + /** + * @brief Retrieve the tree corresponding to this entry. Returns a null pointer + * if this entry corresponds to a file. + * + * @return the tree corresponding to this entry, or a null pointer if + * isDir() is false. + */ + virtual std::shared_ptr astree() const override + { + return std::dynamic_pointer_cast(shared_from_this()); + } + +public: // Destructor: + virtual ~IFileTree() {} + +public: // Deleted operators: + IFileTree(IFileTree const&) = delete; + IFileTree(IFileTree&&) = delete; + + IFileTree& operator=(IFileTree const&) = delete; + IFileTree& operator=(IFileTree&&) = delete; + + /** + * A few implementation details here for implementing classes. While there are + * multiple virtual public methods, most implementation should not have to + * re-implement them. + * + * There are three pure virtual methods that needs to be implemented by any child + * class: + * - makeDirectory(): used to create directories - this method serves to create + * directory that may or may not existing in the underlying source. Implementing class + * do not have to rely on this for `doPopulate()`. This method is also called when new + * directory needs to be created (addDirectory, insert, createOrphanTree, + * merge, etc.), and may return a null pointer to indicate that the operations failed + * or is not permitted. + * - doClone(): called when a tree needs to be cloned (e.g., for a copy) - this + * methods does not copy the subtrees, it should only create an empty tree equivalent + * to the current tree. + * - doPopulate(): called when a tree have to be populated. + * + * The other commons methods that can be re-implemented are: + * - makeFile(), this is used to create new file - this is very similar to + * makeDirectory() except that it has a default implementation that simply creates a + * FileTreeEntry. + * - beforeInsert(), beforeReplace() and beforeRemove(): these can be implemented to + * 1) prevent some operations, 2) perform operations on the actual tree (e.g., move a + * file on the disk). + */ +protected: + friend class FileTreeEntry; + + /** + * Split the given path into parts. + * + * @param path The path to split. + * + * @return a list containing the section of the path. + */ + static QStringList splitPath(QString path); + + /** + * @brief Called before replacing an entry with another one. + * + * This is a pre-method meaning that it can be used to prevent an operation on + * the tree. + * + * This method is for internal usage only and is called when an entry is going to + * be replaced by another (because there is name conflict). + * + * The base implementation of this method does nothing (the actual replacement is + * made elsewhere). This method can be used to prevent a replacement by returning + * false. + * + * @param dstTree Tree containing the destination entry. + * @param destination Entry that will be replaced. + * @param source Entry that will replace the destination. + * + * @return true if the entry can be replaced, false otherwize. + */ + virtual bool beforeReplace(IFileTree const* dstTree, FileTreeEntry const* destination, + FileTreeEntry const* source); + + /** + * @brief Called before inserting an entry in a tree. + * + * This is a pre-method meaning that it can be used to prevent an operation on + * the tree. + * + * This method is for internal usage only and is called when an entry is going to + * be inserted in a tree. This method is not called after makeFile() or + * makeDirectory() so those should be used to prevent creation of files and + * directories. + * + * The base implementation of this method does nothing (the actual insertion is + * made elsewhere). This method can be used to prevent an insertion by returning + * false. + * + * @param tree Tree into which the entry will be inserted. + * @param source Entry that will be inserted the destination. + * + * @return true if the entry can be inserted, false otherwize. + */ + virtual bool beforeInsert(IFileTree const* entry, FileTreeEntry const* name); + + /** + * @brief Called before removing an entry. + * + * This is a pre-method meaning that it can be used to prevent an operation on + * the tree. + * + * This method is for internal usage only and is called when an entry is going to + * be removed from a tree. + * + * The base implementation of this method does nothing (the actual removal is + * made elsewhere). This method can be used to prevent it by returning false. + * + * @param tree Tree containing the entry. + * @param entry Entry that will be removed. + * + * @return true if the entry can be removed, false otherwize. + */ + virtual bool beforeRemove(IFileTree const* entry, FileTreeEntry const* name); + + /** + * @brief Create a new file under this tree. + * + * @param parent The current tree, without const-qualification. + * @param name Name of the file. + * + * @return the created file. + */ + virtual std::shared_ptr + makeFile(std::shared_ptr parent, QString name) const; + + /** + * @brief Create a new entry corresponding to a subtree under this tree. + * + * @param parent The current tree, without const-qualification. + * @param name Name of the directory. + * + * @return the entry for the created directory. + */ + virtual std::shared_ptr + makeDirectory(std::shared_ptr parent, QString name) const = 0; + + /** + * @brief Method that child classes should implement. + * + * This method should populate the given entries of this tree by using the makeFile + * and makeTree method. The usage of the makeFile and makeTree method is not mandatory + * here. + * + * If the implementation can populate the vector of entries in order, it is possible + * to return false to tell IFileTree not to re-sort the vector. If sorted, directories + * should be before files, and both directories and files should be sorted by name in + * a case-insensitive way. + * + * @param parent The current tree, without const-qualification. + * @param entries Vector of entries to populate. + * + * @return true if the vector of entries is already sorted, false if it must be + * sorted. + */ + virtual bool + doPopulate(std::shared_ptr parent, + std::vector>& entries) const = 0; + + /** + * @brief Creates a copy of this file tree. + * + * This methods is called by clone() in order to copy child class attributes. This + * method should basically returns a new copy of the tree with the attributes added by + * the implementation copied (and nothing else). + * + * @return the cloned tree. + */ + virtual std::shared_ptr doClone() const = 0; + +protected: // Constructor + /** + * @brief Creates a new tree. This method takes no parameter since, due to the virtual + * inheritance, child classes must directly call the FileTreeEntry constructor. + */ + IFileTree(); + + /** + * + */ + std::shared_ptr clone() const override; + + /** + * + */ +private: // Private stuff, look away! + /** + * @brief Retrieve the entry corresponding to the given path. + * + * @param path Path to entry. + * @param matchType Type of file to check. + * + * @return the entry, or a null pointer if the entry did not exist. + */ + std::shared_ptr fetchEntry(QStringList path, FileTypes matchType); + std::shared_ptr fetchEntry(QStringList const& path, + FileTypes matchType) const; + + /** + * @brief Merge the source tree into the destination tree. On conflict, the source + * entries are always chosen. + * + * @param destination Destination tree. + * @param source Source tree. + * + * @return the number of overwritten entries. + */ + std::size_t mergeTree(std::shared_ptr destination, + std::shared_ptr source, OverwritesType* overwrites); + + /** + * @brief Create a new subtree under the given tree. + * + * This method will create missing folders in the given path and will not fail if the + * directory already exists but will fail the given path contains "." or "..". + * + * @param begin, end Range of section of the path. + * + * @return the entry corresponding to the create tree, or a null pointer if the tree + * was not created. + */ + std::shared_ptr createTree(QStringList::const_iterator begin, + QStringList::const_iterator end); + + // Indicate if this tree has been populated: + mutable std::atomic m_Populated{false}; + mutable std::once_flag m_OnceFlag; + mutable std::vector> m_Entries; + + /** + * @brief Retrieve the vector of entries after populating it if required. + * + * @return the vector of entries. + */ + std::vector>& entries(); + const std::vector>& entries() const; + + /** + * @brief Populate the internal vectors and update the flag. + */ + void populate() const; +}; + +} // namespace MOBase + +// __has_cpp_attribute(__cpp_lib_generator) does not seem to work, maybe some conflict +// with Qt? +#ifdef __cpp_lib_generator + +#include "ifiletree_utils.h" + +#endif + +#endif diff --git a/libs/uibase/include/uibase/ifiletree_utils.h b/libs/uibase/include/uibase/ifiletree_utils.h new file mode 100644 index 0000000..9340788 --- /dev/null +++ b/libs/uibase/include/uibase/ifiletree_utils.h @@ -0,0 +1,82 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2020 MO2 Team. 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 UIBASE_IFILETREE_UTILS_H +#define UIBASE_IFILETREE_UTILS_H + +#include + +#include + +#include "dllimport.h" +#include "exceptions.h" +#include "ifiletree.h" + +namespace MOBase +{ + +/** + * @brief Exception thrown when an invalid glob pattern is specified. + */ +struct QDLLEXPORT InvalidGlobPatternException : public Exception +{ + using Exception::Exception; +}; + +enum class GlobPatternType +{ + /** + * @brief Glob mode, similar to python pathlib.Path.glob function + */ + GLOB, + + /** + * @brief Regex mode, each part of the pattern (between / or \) is considered a + * regex, except for ** which is still considered as glob. + */ + REGEX +}; + +/** + * @brief Walk this tree, returning entries. + * + * During the walk, parent tree are guaranteed to be visited before their childrens. + * The current tree is not included in the return generator. + * + * @return a generator over the entries. + */ +QDLLEXPORT std::generator> +walk(std::shared_ptr fileTree); + +/** + * @brief Glob entries matching the given pattern in this tree. + * + * @param pattern Glob pattern to match, using the same syntax as QRegularExpression. + * @param patternType Type of the pattern. + * + * @return a generator over the entries matching the given pattern. + */ +QDLLEXPORT std::generator> +glob(std::shared_ptr fileTree, QString pattern, + GlobPatternType patternType = GlobPatternType::GLOB); + +} // namespace MOBase + +#endif diff --git a/libs/uibase/include/uibase/iinstallationmanager.h b/libs/uibase/include/uibase/iinstallationmanager.h new file mode 100644 index 0000000..ef02b29 --- /dev/null +++ b/libs/uibase/include/uibase/iinstallationmanager.h @@ -0,0 +1,128 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IINSTALLATIONMANAGER_H +#define IINSTALLATIONMANAGER_H + +#include +#include + +#include "ifiletree.h" +#include "iplugininstaller.h" + +namespace MOBase +{ + +template +class GuessedValue; + +/** + * @brief The IInstallationManager class. + */ +class IInstallationManager +{ + +public: + virtual ~IInstallationManager() {} + + /** + * @return the extensions of archives supported by this installation manager. + */ + virtual QStringList getSupportedExtensions() const = 0; + + /** + * @brief Extract the specified file from the currently opened archive to a temporary + * location. + * + * This method cannot be used to extract directory. + * + * @param entry Entry corresponding to the file to extract. + * @param silent If true, the dialog showing extraction progress will not be shown. + * + * @return the absolute path to the temporary file, or an empty string if the + * file was not extracted. + * + * @note The call will fail with an exception if no archive is open (plugins deriving + * from IPluginInstallerSimple can rely on that, custom installers should not). + * @note The temporary file is automatically cleaned up after the installation. + * @note This call can be very slow if the archive is large and "solid". + */ + virtual QString extractFile(std::shared_ptr entry, + bool silent = false) = 0; + + /** + * @brief Extract the specified files from the currently opened archive to a temporary + * location. + * + * @param entres Entries corresponding to the files to extract. + * @param silent If true, the dialog showing extraction progress will not be shown. + * + * This method cannot be used to extract directory. + * + * @return the list of absolute paths to the temporary files. + * + * @note The call will fail with an exception if no archive is open (plugins deriving + * from IPluginInstallerSimple can rely on that, custom installers should not). + * @note The temporary file is automatically cleaned up after the installation. + * @note This call can be very slow if the archive is large and "solid". + * + * The flatten argument is not present here while it is present in the deprecated + * QStringList version for multiple reasons: 1) it was never used, 2) it is kind of + * fishy because there is no way to know if a file is going to be overriden, 3) it is + * quite easy to flatten a IFileTree and thus to given a list of entries flattened + * (this was not possible with the QStringList version since these were based on the + * name of the file inside the archive). + */ + virtual QStringList + extractFiles(std::vector> const& entries, + bool silent = false) = 0; + + /** + * @brief Create a new file on the disk corresponding to the given entry. + * + * This method can be used by installer that needs to create files that are not in the + * original archive. At the end of the installation, if there are entries in the final + * tree that were used to create files, the corresponding files will be moved to the + * mod folder. + * + * @param entry The entry for which a temporary file should be created. + * + * @return the path to the created file, or an empty QString() if the file could not + * be created. + */ + virtual QString createFile(std::shared_ptr entry) = 0; + + /** + * @brief Installs the given archive. + * + * @param modName Suggested name of the mod. + * @param archiveFile Path to the archive to install. + * @param modId ID of the mod, if available. + * + * @return the installation result. + */ + virtual IPluginInstaller::EInstallResult + installArchive(MOBase::GuessedValue& modName, const QString& archiveFile, + int modID = 0) = 0; +}; + +} // namespace MOBase + +#endif // IINSTALLATIONMANAGER_H diff --git a/libs/uibase/include/uibase/iinstance.h b/libs/uibase/include/uibase/iinstance.h new file mode 100644 index 0000000..1c59d4a --- /dev/null +++ b/libs/uibase/include/uibase/iinstance.h @@ -0,0 +1,61 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IINSTANCE_H +#define IINSTANCE_H + +#include + +namespace MOBase +{ +class IPluginGame; + +/** + * @brief Represents a Mod Organizer instance, either global or portable. + */ +class IInstance +{ +public: + /** + * @return The instance name; this is the directory name or "Portable" for portable + * instances. + */ + virtual QString displayName() const = 0; + + /** + * @return The name of the game managed by this instance, or an empty string if the + * INI file could not be read. + */ + virtual QString gameName() const = 0; + + /** + * @return The directory where the game is installed, or an empty string if the INI + * file could not be read. + */ + virtual QString gameDirectory() const = 0; + + /** + * @return true if this is a portable instance, false if it is a global one. + */ + virtual bool isPortable() const = 0; +}; +} // namespace MOBase + +#endif // IINSTANCE_H diff --git a/libs/uibase/include/uibase/iinstancemanager.h b/libs/uibase/include/uibase/iinstancemanager.h new file mode 100644 index 0000000..c57479a --- /dev/null +++ b/libs/uibase/include/uibase/iinstancemanager.h @@ -0,0 +1,66 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IINSTANCEMANAGER_H +#define IINSTANCEMANAGER_H + +#include +#include + +#include +#include + +namespace MOBase +{ +class IInstance; + +/** + * @brief Interface to the instance manager of Mod Organizer. + */ +class IInstanceManager +{ +public: + /** + * @return the current instance. + */ + virtual std::shared_ptr currentInstance() const = 0; + + /** + * @return The list of absolute paths to all global instances. + * + * @note This does not include portable instances. + */ + virtual std::vector globalInstancePaths() const = 0; + + /** + * @brief Retrieve the global instance corresponding to the given name. + * + * @param instanceName Name of the global instance to retrieve. This is the directory + * name of the instance. + * + * @return the global instance corresponding to the given name, or nullptr if no such + * instance exists. + */ + virtual std::shared_ptr + getGlobalInstance(const QString& instanceName) const = 0; +}; +} // namespace MOBase + +#endif // IINSTANCEMANAGER_H diff --git a/libs/uibase/include/uibase/imodinterface.h b/libs/uibase/include/uibase/imodinterface.h new file mode 100644 index 0000000..a2684b4 --- /dev/null +++ b/libs/uibase/include/uibase/imodinterface.h @@ -0,0 +1,343 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IMODINTERFACE_H +#define IMODINTERFACE_H + +#include +#include +#include + +#include +#include +#include +#include + +namespace MOBase +{ + +class VersionInfo; +class IFileTree; + +enum class EndorsedState +{ + ENDORSED_FALSE, + ENDORSED_TRUE, + ENDORSED_UNKNOWN, + ENDORSED_NEVER +}; + +enum class TrackedState +{ + TRACKED_FALSE, + TRACKED_TRUE, + TRACKED_UNKNOWN, +}; + +class IModInterface +{ +public: + virtual ~IModInterface() {} + +public: // Non-meta related information: + /** + * @return the name of the mod. + */ + virtual QString name() const = 0; + + /** + * @return the absolute path to the mod to be used in file system operations. + */ + virtual QString absolutePath() const = 0; + +public: // Meta-related information: + /** + * @return the comments for this mod, if any. + */ + virtual QString comments() const = 0; + + /** + * @return the notes for this mod, if any. + */ + virtual QString notes() const = 0; + + /** + * @brief Retrieve the short name of the game associated with this mod. This may + * differ from the current game plugin (e.g. you can install a Skyrim LE game in a SSE + * installation). + * + * @return the name of the game associated with this mod. + */ + virtual QString gameName() const = 0; + + /** + * @return the name of the repository from which this mod was installed. + */ + virtual QString repository() const = 0; + + /** + * @return the Nexus ID of this mod. + */ + virtual int nexusId() const = 0; + + /** + * @return the current version of this mod. + */ + virtual VersionInfo version() const = 0; + + /** + * @return the newest version of thid mod (as known by MO2). If this matches + * version(), then the mod is up-to-date. + */ + virtual VersionInfo newestVersion() const = 0; + + /** + * @return the ignored version of this mod (for update), or an invalid version if the + * user did not ignore version for this mod. + */ + virtual VersionInfo ignoredVersion() const = 0; + + /** + * @return the absolute path to the file that was used to install this mod. + */ + virtual QString installationFile() const = 0; + + virtual std::set> installedFiles() const = 0; + + /** + * @return true if this mod was marked as converted by the user. + * + * @note When a mod is for a different game, a flag is shown to users to warn them, + * but they can mark mods as converted to remove this flag. + */ + virtual bool converted() const = 0; + + /** + * @return true if th is mod was marked as containing valid game data. + * + * @note MO2 uses ModDataChecker to check the content of mods, but sometimes these + * fail, in which case mods are incorrectly marked as 'not containing valid games + * data'. Users can choose to mark these mods as valid to hide the warning / flag. + */ + virtual bool validated() const = 0; + + /** + * @return the color of the 'Notes' column chosen by the user. + */ + virtual QColor color() const = 0; + + /** + * @return the URL of this mod, or an empty QString() if no URL is associated + * with this mod. + */ + virtual QString url() const = 0; + + /** + * @return the ID of the primary category of this mod. + */ + virtual int primaryCategory() const = 0; + + /** + * @return the list of categories this mod belongs to. + */ + virtual QStringList categories() const = 0; + + /** + * @return the mod author. + */ + virtual QString author() const = 0; + + /** + * @return the mod uploader. + */ + virtual QString uploader() const = 0; + + /** + * @return the URL of the uploader. + */ + virtual QString uploaderUrl() const = 0; + + /** + * @return the tracked state of this mod. + */ + virtual TrackedState trackedState() const = 0; + + /** + * @return the endorsement state of this mod. + */ + virtual EndorsedState endorsedState() const = 0; + + /** + * @brief Retrieve a file tree corresponding to the underlying disk content + * of this mod. + * + * The file tree should not be cached since it is already cached and updated when + * required. + * + * @return a file tree representing the content of this mod. + */ + virtual std::shared_ptr fileTree() const = 0; + + /** + * @return true if this object represents the overwrite mod. + */ + virtual bool isOverwrite() const = 0; + + /** + * @return true if this object represents a backup. + */ + virtual bool isBackup() const = 0; + + /** + * @return true if this object represents a separator. + */ + virtual bool isSeparator() const = 0; + + /** + * @return true if this object represents a foreign mod. + */ + virtual bool isForeign() const = 0; + +public: // Mutable operations: + /** + * @brief set/change the version of this mod + * @param version new version of the mod + */ + virtual void setVersion(const VersionInfo& version) = 0; + + /** + * @brief sets the installation file for this mod + * @param fileName archive file name + */ + virtual void setInstallationFile(const QString& fileName) = 0; + + /** + * @brief set/change the latest known version of this mod + * @param version newest known version of the mod + */ + virtual void setNewestVersion(const VersionInfo& version) = 0; + + /** + * @brief set endorsement state of the mod + * @param endorsed new endorsement state + */ + virtual void setIsEndorsed(bool endorsed) = 0; + + /** + * @brief sets the mod id on nexus for this mod + * @param the new id to set + */ + virtual void setNexusID(int nexusID) = 0; + + /** + * @brief sets the category id from a nexus category id. Conversion to MO id happens + * internally + * @param categoryID the nexus category id + * @note if a mapping is not possible, the category is set to the default value + */ + virtual void addNexusCategory(int categoryID) = 0; + + /** + * @brief assign a category to the mod. If the named category doesn't exist it is + * created + * @param categoryName name of the new category + */ + virtual void addCategory(const QString& categoryName) = 0; + + /** + * @brief unassign a category from this mod. + * @param categoryName name of the category to be removed + * @return true if the category was removed successfully, false if no such category + * was assigned + */ + virtual bool removeCategory(const QString& categoryName) = 0; + + /** + * @brief set/change the source game of this mod + * + * @param gameName the source game shortName + */ + virtual void setGameName(const QString& gameName) = 0; + + /** + * @brief Set a URL for this mod. + * + * @param url The URL of this mod. + */ + virtual void setUrl(const QString& url) = 0; + +public: // Plugin operations: + /** + * @brief Retrieve the specified setting in this mod for a plugin. + * + * @param pluginName Name of the plugin for which to retrieve a setting. This should + * always be IPlugin::name() unless you have a really good reason to access + * settings of another plugin. + * @param key Identifier of the setting. + * @param defaultValue The default value to return if the setting does not exist. + * + * @return the setting, if found, or the default value. + */ + virtual QVariant pluginSetting(const QString& pluginName, const QString& key, + const QVariant& defaultValue = QVariant()) const = 0; + + /** + * @brief Retrieve the settings in this mod for a plugin. + * + * @param pluginName Name of the plugin for which to retrieve settings. This should + * always be IPlugin::name() unless you have a really good reason to access settings + * of another plugin. + * + * @return a map from setting key to value. The map is empty if there are not settings + * for this mod. + */ + virtual std::map + pluginSettings(const QString& pluginName) const = 0; + + /** + * @brief Set the specified setting in this mod for a plugin. + * + * @param pluginName Name of the plugin for which to retrieve a setting. This should + * always be IPlugin::name() unless you have a really good reason to access settings + * of another plugin. + * @param key Identifier of the setting. + * @param value New value for the setting to set. + * + * @return true if the setting was set correctly, false otherwise. + */ + virtual bool setPluginSetting(const QString& pluginName, const QString& key, + const QVariant& value) = 0; + + /** + * @brief Remove all the settings of the specified plugin from th is mod. + * + * @param pluginName Name of the plugin for which settings should be removed. This + * should always be IPlugin::name() unless you have a really good reason to access + * settings of another plugin. + * + * @return the old settings from the given plugin, as returned by `pluginSettings()`. + */ + virtual std::map + clearPluginSettings(const QString& pluginName) = 0; +}; + +} // namespace MOBase + +#endif // IMODINTERFACE_H diff --git a/libs/uibase/include/uibase/imodlist.h b/libs/uibase/include/uibase/imodlist.h new file mode 100644 index 0000000..7f7a2a1 --- /dev/null +++ b/libs/uibase/include/uibase/imodlist.h @@ -0,0 +1,229 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2013 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 IMODLIST_H +#define IMODLIST_H + +#include +#include + +#include +#include + +#include "imodinterface.h" +#include "iprofile.h" + +namespace MOBase +{ + +/** + * @brief interface to the mod-list + * @note all api functions in this interface work need the internal name of a mod to + * find a mod. For regular mods (mods the user installed) the display name (as shown to + * the user) and internal name are identical. For other mods (non-MO mods) there is + * currently no way to translate from display name to internal name because the display + * name might not me un-ambiguous. + */ +class IModList +{ + +public: + enum ModState + { + STATE_EXISTS = 0x00000001, + STATE_ACTIVE = 0x00000002, + STATE_ESSENTIAL = 0x00000004, + STATE_EMPTY = 0x00000008, + STATE_ENDORSED = 0x00000010, + STATE_VALID = 0x00000020, + STATE_ALTERNATE = 0x00000040 + }; + + Q_DECLARE_FLAGS(ModStates, ModState) + +public: + virtual ~IModList() {} + + /** + * @brief retrieves the display name of a mod from it's internal name + * @param internalName the internal name + * @return a string intended to identify the name to the user + * @note If you received an internal name from an api (i.e. IPluginList::origin) then + * you should use that name to identify the mod to all other api calls but use this + * function to retrieve the name to show to the user. + */ + virtual QString displayName(const QString& internalName) const = 0; + + /** + * @brief Retrieve a list of all installed mod names. + * + * @return list of mods (internal names). + */ + virtual QStringList allMods() const = 0; + + /** + * @brief Retrieve the list of installed mod names, sorted by current profile + * priority. + * + * @param profile The profile to use for the priority. If nullptr, the current + * profile is used. + * + * @return list of mods (internal names), sorted by priority. + */ + virtual QStringList + allModsByProfilePriority(MOBase::IProfile* profile = nullptr) const = 0; + + /** + * @brief Retrieve the mod with the given name. + * + * @param name Name of the mod to retrieve. + * + * @return the mod with the given name, or a null pointer if there is no mod with this + * name. + */ + virtual IModInterface* getMod(const QString& name) const = 0; + + /** + * @brief Remove a mod (from disc and from the ui). + * + * @param mod The mod to remove. + * + * @return true on success, false on error. + */ + virtual bool removeMod(MOBase::IModInterface* mod) = 0; + + /** + * @brief Rename the given mod. + * + * This invalidate the mod so you should use the returned value afterwards. + * + * @param mod The mod to rename. + * + * @return the new mod (after renaming) on success, a null pointer on error. + */ + virtual MOBase::IModInterface* renameMod(MOBase::IModInterface* mod, + const QString& name) = 0; + + /** + * @brief retrieve the state of a mod + * @param name name of the mod + * @return a bitset of information about the mod + */ + virtual ModStates state(const QString& name) const = 0; + + /** + * @brief enable or disable a mod + * + * @param name name of the mod + * @param active if true the mod is enabled, otherwise it's disabled + * + * @return true on success, false if the mod name is not valid + * + * @note calling this will cause MO to re-evaluate its virtual file system so this is + * fairly expensive + */ + virtual bool setActive(const QString& name, bool active) = 0; + + /** + * @brief enable or disable a list of mod + * + * @param names names of the mod + * @param active if true mods are enabled, otherwise they are disabled + * + * @return the number of mods successfully enabled or disabled + * + * @note calling this will cause MO to re-evaluate its virtual file system so this is + * fairly expensive + */ + virtual int setActive(const QStringList& names, bool active) = 0; + + /** + * @brief retrieve the priority of a mod + * @param name name of the mod + * @return the priority (the higher the more important). Returns -1 if the mod doesn't + * exist + */ + virtual int priority(const QString& name) const = 0; + + /** + * @brief change the priority of a mod + * @param name name of the mod + * @param newPriority new priority of the mod + * @return true on success, false if the priority change was not possible. This is + * usually because one of the parameters is invalid + * @note Very important: newPriority is the new priority after the move. Keep in mind + * that the mod disappears from it's old location and all mods with higher priority + * than the moved mod decrease in priority by one. + */ + virtual bool setPriority(const QString& name, int newPriority) = 0; + + /** + * @brief Add a new callback to be called when a new mod is installed. + * + * Parameters of the callback: + * - The installed mod. + * + * @param func Function to called when a mod has been installed. + */ + virtual bool onModInstalled(const std::function& func) = 0; + + /** + * @brief Add a new callback to be called when a mod is removed. + * + * Parameters of the callback: + * - The name of the removed mod. + * + * @param func Function to called when a mod has been removed. + */ + virtual bool onModRemoved(const std::function& func) = 0; + + /** + * @brief Installs a handler for the event that the state of mods changed + * (enabled/disabled, endorsed, ...). + * + * Parameters of the callback: + * - Map containing the mods whose states have changed. Keys are mod names and + * values are mod states. + * + * @param func The signal to be called when the state of any mod changes. + * + * @return true if the handler was successfully installed, false otherwise (there is + * as of now no known reason this should fail). + */ + virtual bool onModStateChanged( + const std::function&)>& func) = 0; + + /** + * installs a handler for the event that a mod changes priority + * @param func the signal to be called when the priority of a mod changes + * (first parameter for the handler is the name of the mod, second is the old + * priority, third the new one) + * @return true if the handler was successfully installed (there is as of now no known + * reason this should fail) + */ + virtual bool + onModMoved(const std::function& func) = 0; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(IModList::ModStates) + +} // namespace MOBase + +#endif // IMODLIST_H diff --git a/libs/uibase/include/uibase/imodrepositorybridge.h b/libs/uibase/include/uibase/imodrepositorybridge.h new file mode 100644 index 0000000..f2bf539 --- /dev/null +++ b/libs/uibase/include/uibase/imodrepositorybridge.h @@ -0,0 +1,219 @@ +#ifndef INEXUSBRIDGE_H +#define INEXUSBRIDGE_H + +#include "modrepositoryfileinfo.h" +#include +#include +#include + +namespace MOBase +{ + +class QDLLEXPORT IModRepositoryBridge : public QObject +{ + Q_OBJECT +public: + IModRepositoryBridge(QObject* parent = nullptr) : QObject(parent) {} + + /** + * @brief request description for a mod + * + * @param modID id of the mod caller is interested in + * @param userData user data to be returned with the result + **/ + virtual void requestDescription(QString gameName, int modID, QVariant userData) = 0; + + /** + * @brief request a list of the files belonging to a mod + * + * @param modID id of the mod caller is interested in + * @param userData user data to be returned with the result + **/ + virtual void requestFiles(QString gameName, int modID, QVariant userData) = 0; + + /** + * @brief request info about a single file of a mod + * + * @param modID id of the mod caller is interested in + * @param fileID id of the file the caller is interested in + * @param userData user data to be returned with the result + **/ + virtual void requestFileInfo(QString game, int modID, int fileID, + QVariant userData) = 0; + + /** + * @brief request the download url of a file + * + * @param modID id of the mod caller is interested in + * @param fileID id of the file the caller is interested in + * @param userData user data to be returned with the result + **/ + virtual void requestDownloadURL(QString gameName, int modID, int fileID, + QVariant userData) = 0; + + /** + * @brief requestToggleEndorsement + * @param modID id of the mod caller is interested in + * @param userData user data to be returned with the result + */ + virtual void requestToggleEndorsement(QString gameName, int modID, QString modVersion, + bool endorse, QVariant userData) = 0; + +private: + Q_DISABLE_COPY(IModRepositoryBridge) + +Q_SIGNALS: + + /** + * @brief sent when the description for a mod is reported by the repository + * @param modID id of the mod for which the request was made + * @param userData the data that was included in the request + * @param resultData result data. this is a variant map. keys are strings. Value is + * usually also a string, wrapped in a variant. + * @note valid keys might change as the repository page is updated. For nexus, the + * following keys are valid as of this writing: 'allow_view', 'ip', + * 'one_week_ratings', 'date', 'pm_notify', 'OLD_mid', 'OLD_u_downloads', 'game_id', + * 'OLD_perm_use', 'mod_page_uri', 'allow_topics', 'has_hot_image', 'id', + * 'two_weeks_ratings', 'description', 'lastupdate', 'perm_convert', 'author', + * 'OLD_image', 'translation_of', 'OLD_mname', 'version', 'allow_rating', + * 'perm_useinstructions', 'featured_count', 'donate', 'type', 'perm_credits', + * 'hidden_reason', 'OLD_views', 'perm_upload', 'has_back_image', 'adult', + * 'allow_images', 'OLD_endorsements', 'OLD_size', 'name', 'commenting', 'moderate', + * 'language', 'perm_others', 'lastcomment', 'OLD_readme', 'summary', 'perm_modify', + * 'OLD_downloads', 'lock_comments', 'suggested_category', 'allow_tagging', + * 'published', 'perm_notes', 'category_id', 'thread_id', 'perm_use', 'wizard_steps' + * @note in the python interface use the onDescriptionAvailable call to register a + * callback for this signal. The signature of your callback must match the signature + * of this signal + * @note this interface is going to be changed at some point to replace resultData + * with a less "dynamic" data structure + */ + void descriptionAvailable(QString gameName, int modID, QVariant userData, + QVariant resultData); + + /** + * @brief sent when the list of files for a mod is reported by the repository + * @param modID id of the mod for which the request was made + * @param userData the data that was included in the request + * @param resultData a list of (already decoded) file information objects + * @note in the python interface use the onFilesAvailable call to register a callback + * for this signal. The signature of your callback must match the signature of this + * signal + */ + void filesAvailable(QString gameName, int modID, QVariant userData, + const QList& resultData); + + /** + * @brief sent when information about a file is reported by the repository + * @param modID id of the mod for which the request was made + * @param fileID id of the the file for which information was requested + * @param userData the data that was included in the request + * @param resultData a variant map of information about the file. + * @note valid keys might change as the repository page is updated. For nexus, the + * following keys are valid as of this writing: 'count', 'requirements_alert', + * 'u_count', 'description', 'uri', 'size', 'owner_id', 'primary', 'manager', + * 'version', 'date', 'game_id', 'mod_id', 'category_id', 'id', 'name' + * @note in the python interface use the onFileInfoAvailable call to register a + * callback for this signal. The signature of your callback must match the signature + * of this signal + * @note if you intend to download this file you don't have to request this + * information manually. call IDownloadManager::startDownloadNexusFile and let the + * download manager figure things out + * @note this interface is going to be changed at some point to replace resultData + * with a less "dynamic" data structure + */ + void fileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, + QVariant resultData); + + /** + * @brief sent when the list of download urls for a file is returned by the repository + * @param modID id of the mod for which the request was made + * @param fileID id of the file for which the downloads + * @param userData the data that was included in the request + * @param resultData a variant map of information about the url + * @note this function is not exposed to python. please use + * IDownloadManager::startDownloadNexusFile to download a file, this lets the download + * manager figure out the best server according to user preference and stuff + * @note this interface is going to be changed at some point to replace resultData + * with a less "dynamic" data structure + */ + void downloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, + QVariant resultData); + + /** + * @brief sent when the endorsement data is returned from the API + * @param userData the data that was included in the request + * @param resultData new endorsement state as a boolean (wrapped in a qvariant) + * @note in the python interface use the onEndorsementsAvailable call to register a + * callback for this signal. The signature of your callback must match the signature + * of this signal + */ + void endorsementsAvailable(QVariant userData, QVariant resultData); + + /** + * @brief sent when the endorsement state of a mod was changed (only sent as a result + * of our request) + * @param modID id of the mod for which the request was made + * @param userData the data that was included in the request + * @param resultData new endorsement state as a boolean (wrapped in a qvariant) + * @note in the python interface use the onEndorsementToggled call to register a + * callback for this signal. The signature of your callback must match the signature + * of this signal + * @note this interface is going to be changed at some point to replace resultData + * with a boolean + */ + void endorsementToggled(QString gameName, int modID, QVariant userData, + QVariant resultData); + + /** + * @brief sent when the tracked mod data is returned from the API + * @param userData the data that was included in the request + * @param resultData new tracked state as a list of maps (keys: domain_name, mod_id) + * @note in the python interface use the ontrackedModsAvailable call to register a + * callback for this signal. The signature of your callback must match the signature + * of this signal + */ + void trackedModsAvailable(QVariant userData, QVariant resultData); + + /** + * @brief sent when the tracking state of a mod was changed (only sent as a result of + * our request) + * @param modID id of the mod for which the request was made + * @param userData the data that was included in the request + * @param tracked new tracking state + * @note in the python interface use the onTrackingToggled call to register a callback + * for this signal. The signature of your callback must match the signature of this + * signal + */ + void trackingToggled(QString gameName, int modID, QVariant userData, bool tracked); + + /** + * @brief sent when the tracking state of a mod was changed (only sent as a result of + * our request) + * @param modID id of the mod for which the request was made + * @param userData the data that was included in the request + * @param tracked new tracking state + * @note in the python interface use the onTrackingToggled call to register a callback + * for this signal. The signature of your callback must match the signature of this + * signal + */ + void gameInfoAvailable(QString gameName, QVariant userData, QVariant resultData); + + /** + * @brief sent when a request to nexus failed + * @param modID id of the mod for which the request was made + * @param fileID id of the file for which the request was made (ignore if the request + * was for the mod in general) + * @param userData the data that was included in the request + * @param errorMessage textual description of the problem + * @note in the python interface use the onDescriptionAvailable call to register a + * callback for this signal. The signature of your callback must match the signature + * of this signal + */ + void requestFailed(QString gameName, int modID, int fileID, QVariant userData, + int errorCode, const QString& errorMessage); +}; + +} // namespace MOBase + +#endif // INEXUSBRIDGE_H diff --git a/libs/uibase/include/uibase/imoinfo.h b/libs/uibase/include/uibase/imoinfo.h new file mode 100644 index 0000000..a9ec3d8 --- /dev/null +++ b/libs/uibase/include/uibase/imoinfo.h @@ -0,0 +1,616 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IMOINFO_H +#define IMOINFO_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Include utility.h for HANDLE/DWORD typedefs on Linux +#include "utility.h" + +#include "game_features/game_feature.h" +#include "guessedvalue.h" +#include "imodlist.h" +#include "iprofile.h" +#include "versioninfo.h" +#include "versioning.h" + +namespace MOBase +{ + +class IExecutablesList; +class IFileTree; +class IModInterface; +class IModRepositoryBridge; +class IDownloadManager; +class IInstanceManager; +class IPluginList; +class IPlugin; +class IPluginGame; +class IGameFeatures; + +/** + * @brief Interface to class that provides information about the running session + * of Mod Organizer to be used by plugins + * + * When MO requires plugins but does not have a valid instance loaded (such as + * on first start in the instance creation dialog), init() will not be called at + * all, except for proxy plugins. + * + * In the case of proxy plugins, init() is called with a null IOrganizer. + */ +class QDLLEXPORT IOrganizer : public QObject +{ + Q_OBJECT + +public: + /** + * @brief information about a virtualised file + */ + struct FileInfo + { + QString filePath; /// full path to the file + QString archive; /// name of the archive if this file is in a BSA, otherwise this + /// is empty. + QStringList origins; /// list of origins containing this file. the first one is the + /// highest priority one + }; + +public: + virtual ~IOrganizer() {} + + // the directory for plugin data, typically plugins/data + // + static QString getPluginDataPath(); + + /** + * @return create a new nexus interface class + */ + virtual IModRepositoryBridge* createNexusBridge() const = 0; + + /** + * @return the name of the current instance (the directory name or "Portable" for + * portable instances) + */ + virtual QString instanceName() const = 0; + + /** + * @return name of the active profile or an empty string if no profile is loaded (yet) + */ + virtual QString profileName() const = 0; + + /** + * @return the (absolute) path to the active profile or an empty string if no profile + * is loaded (yet) + */ + virtual QString profilePath() const = 0; + + /** + * @return the (absolute) path to the download directory + */ + virtual QString downloadsPath() const = 0; + + /** + * @return the (absolute) path to the overwrite directory + */ + virtual QString overwritePath() const = 0; + + /** + * @return the (absolute) path to the base directory + */ + virtual QString basePath() const = 0; + + /** + * @return the (absolute) path to the mods directory + */ + virtual QString modsPath() const = 0; + + /** + * @return the running version of Mod Organizer + */ + [[deprecated]] virtual VersionInfo appVersion() const = 0; + + /** + * @return the running version of Mod Organizer + */ + virtual Version version() const = 0; + + /** + * @brief create a new mod with the specified name + * @param name name of the new mod + * @return an interface that can be used to modify the mod. nullptr if the user + * canceled + * @note a popup asking the user to merge, rename or replace the mod is displayed if + * the mod already exists. That has to happen on the main thread and MO2 will deadlock + * if it happens on any other. If this needs to be called from another thread, use + * IModList::getMod() to verify the mod-name is unused first + */ + virtual IModInterface* createMod(GuessedValue& name) = 0; + + /** + * @brief get the game plugin matching the specified game + * @param gameName name of the game short name + * @return a game plugin, or nullptr if there is no match + */ + virtual IPluginGame* getGame(const QString& gameName) const = 0; + + /** + * @brief let the organizer know that a mod has changed + * @param the mod that has changed + */ + virtual void modDataChanged(IModInterface* mod) = 0; + + /** + * @brief Check if a plugin is enabled. + * + * @param pluginName Plugin to check. + * + * @return true if the plugin is enabled, false otherwise. + */ + virtual bool isPluginEnabled(IPlugin* plugin) const = 0; + + /** + * @brief Check if a plugin is enabled. + * + * @param pluginName Name of the plugin to check. + * + * @return true if the plugin is enabled, false otherwise. + */ + virtual bool isPluginEnabled(QString const& pluginName) const = 0; + + /** + * @brief Retrieve the specified setting for a plugin. + * + * @param pluginName Name of the plugin for which to retrieve a setting. This should + * always be IPlugin::name() unless you have a really good reason to access settings + * of another mod. You can not access settings for a plugin that isn't installed. + * @param key identifier of the setting + * @return the setting + * @note an invalid qvariant is returned if the setting has not been declared + */ + virtual QVariant pluginSetting(const QString& pluginName, + const QString& key) const = 0; + + /** + * @brief Set the specified setting for a plugin. + * + * This automatically emit pluginSettingChanged(), so you do not have to do it + * yourself. + * + * @param pluginName Name of the plugin for which to change a value. This should + * always be IPlugin::name() unless you have a really good reason to access data of + * another mod AND if you can verify that plugin is actually installed. + * @param key Identifier of the setting. + * @param value Value to set. + * + * @throw an exception is thrown if pluginName doesn't refer to an installed plugin. + */ + virtual void setPluginSetting(const QString& pluginName, const QString& key, + const QVariant& value) = 0; + + /** + * @brief retrieve the specified persistent value for a plugin + * @param pluginName name of the plugin for which to retrieve a value. This should + * always be IPlugin::name() unless you have a really good reason to access data of + * another mod. + * @param key identifier of the value + * @param def default value to return if the key is not (yet) set + * @return the value + * @note A persistent is an arbitrary value that the plugin can set and retrieve that + * is persistently stored by the main application. There is no UI for the user to + * change this value but (s)he can directly access the storage + */ + virtual QVariant persistent(const QString& pluginName, const QString& key, + const QVariant& def = QVariant()) const = 0; + + /** + * @brief set the specified persistent value for a plugin + * @param pluginName name of the plugin for which to change a value. This should + * always be IPlugin::name() unless you have a really good reason to access data of + * another mod AND if you can verify that plugin is actually installed + * @param key identifier of the value + * @param value value to set + * @param sync if true the storage is immediately written to disc. This costs + * performance but is safer against data loss + * @throw an exception is thrown if pluginName doesn't refer to an installed plugin + */ + virtual void setPersistent(const QString& pluginName, const QString& key, + const QVariant& value, bool sync = true) = 0; + + /** + * @return path to a directory where plugin data should be stored. + */ + virtual QString pluginDataPath() const = 0; + + /** + * @brief install a mod archive at the specified location + * @param fileName absolute file name of the mod to install + * @param nameSuggestion suggested name for this mod. This can still be changed by the + * user + * @return interface to the newly installed mod or nullptr if no installation took + * place (failure or use canceled + */ + virtual IModInterface* installMod(const QString& fileName, + const QString& nameSuggestion = QString()) = 0; + + /** + * @brief resolves a path relative to the virtual data directory to its absolute real + * path + * @param fileName path to resolve + * @return the absolute real path or an empty string + */ + virtual QString resolvePath(const QString& fileName) const = 0; + + /** + * @brief retrieves a list of (virtual) subdirectories for a path (relative to the + * data directory) + * @param directoryName relative path to the directory to list + * @return a list of directory names + */ + virtual QStringList listDirectories(const QString& directoryName) const = 0; + + /** + * @brief find files in the virtual directory matching the filename filter + * @param path the path to search in + * @param filter filter function to match against + * @return a list of matching files + */ + virtual QStringList + findFiles(const QString& path, + const std::function& filter) const = 0; + + /** + * @brief find files in the virtual directory matching the filename filter + * @param path the path to search in + * @param filters list of glob filters to match against + * @return a list of matching files + */ + virtual QStringList findFiles(const QString& path, + const QStringList& filters) const = 0; + + /** + * @brief retrieve the file origins for the speicified file. The origins are listed + * with their internal name + * @return list of origins that contain the specified file, sorted by their priority + * @note the internal name of a mod can differ from the display name for + * disambiguation + */ + virtual QStringList getFileOrigins(const QString& fileName) const = 0; + + /** + * @brief find files in the virtual directory matching the specified complex filter + * @param path the path to search in + * @param filter filter function to match against + * @return a list of matching files + * @note this function is more expensive than the one filtering by name so use the + * other one if it suffices + */ + virtual QList + findFileInfos(const QString& path, + const std::function& filter) const = 0; + + /** + * @return a IFileTree representing the virtual file tree. + */ + virtual std::shared_ptr virtualFileTree() const = 0; + + /** + * @return interface to the instance manager + */ + virtual IInstanceManager* instanceManager() const = 0; + + /** + * @return interface to the download manager + */ + virtual IDownloadManager* downloadManager() const = 0; + + /** + * @return interface to the list of plugins (esps, esms, and esls) + */ + virtual IPluginList* pluginList() const = 0; + + /** + * @return interface to the list of mods + */ + virtual IModList* modList() const = 0; + + /** + * @return interface to the list of executables + */ + virtual IExecutablesList* executablesList() const = 0; + + /** + * @return interface to the active profile + */ + virtual std::shared_ptr profile() const = 0; + + /** + * @return list of names of all profiles + */ + virtual QStringList profileNames() const = 0; + + /** + * @brief retrieve a profile by name + * + * @param name name of the profile + * + * @return the profile with the specified name or nullptr if not found + */ + virtual std::shared_ptr getProfile(const QString& name) const = 0; + + /** + * @return interface to game features. + */ + virtual IGameFeatures* gameFeatures() const = 0; + + /** + * @brief runs a program using the virtual filesystem + * + * @param executable either the name of an executable configured in MO, or + * a path to an executable; if relative, it is resolved + * against the game directory + * + * @param args arguments to pass to the executable; if this is empty + * and `executable` refers to a configured executable, + * its arguments are used + * + * @param cwd working directory for the executable; if this is empty, + * it is set to either the cwd set by the user in the + * configured executable (if any) or the directory of the + * executable + * + * @param profile name of the profile to use; defaults to the active + * profile + * + * @param forcedCustomOverwrite the name of the mod to set as the custom + * overwrite directory, regardless of what the + * profile has configured + * + * @param ignoreCustomOverwrite if `executable` is the name of a configured + * executable, ignores the executable's custom + * overwrite + * + * @return a handle to the process that was started or INVALID_HANDLE_VALUE + * if the application failed to start. + */ + virtual HANDLE startApplication(const QString& executable, + const QStringList& args = QStringList(), + const QString& cwd = "", const QString& profile = "", + const QString& forcedCustomOverwrite = "", + bool ignoreCustomOverwrite = false) = 0; + + /** + * @brief blocks until the given process has completed + * + * @param handle the process to wait for + * @param refresh whether MO should refresh after the process completed + * @param exitCode the exit code of the process after it ended + * + * @return true if the process completed successfully + * + * @note this will always show the lock overlay, regardless of whether the + * user has disabled locking in the setting, so use this with care; + * note that the lock overlay will always allow the user to unlock, in + * which case this will return false + */ + virtual bool waitForApplication(HANDLE handle, bool refresh = true, + LPDWORD exitCode = nullptr) const = 0; + + /** + * @brief Refresh the internal mods file structure from disk. This includes the mod + * list, the plugin list, data tab and other smaller things like problems button (same + * as pressing F5). + * + * @note The main part of the refresh of the mods file strcuture, modlist and + * pluginlist is done asynchronously, so you should not expect them to be up-to-date + * when this function returns. + * + * @param saveChanges If true, the relevant profile information is saved first + * (enabled mods and their order). + */ + virtual void refresh(bool saveChanges = true) = 0; + + /** + * @brief get the currently managed game info + */ + virtual MOBase::IPluginGame const* managedGame() const = 0; + + /** + * @brief Add a new callback to be called when an application is about to be run. + * + * Parameters of the callback: + * - Path (absolute) to the application to be run. + * - [Optional] Working directory for the run. + * - [Optional] Argument for the binary. + * + * The callback can return false to prevent the application from being launched. + * + * @param func Function to be called when an application is run. + */ + virtual bool onAboutToRun(const std::function& func) = 0; + virtual bool onAboutToRun( + const std::function& func) = 0; + + /** + * @brief Add a new callback to be called when an has finished running. + * + * Parameters of the callback: + * - Path (absolute) to the application that has finished running. + * - Exit code of the application. + * + * + * @param func Function to be called when an application is run. + */ + virtual bool + onFinishedRun(const std::function& func) = 0; + + /** + * @brief Add a new callback to be called when the user interface has been + * initialized. + * + * Parameters of the callback: + * - The main window of the application. + * + * @param func Function to be called when the user interface has been initialized. + */ + virtual bool + onUserInterfaceInitialized(std::function const& func) = 0; + + /** + * @brief Add a new callback to be called when the next refresh finishes (or + * immediately if possible). + * + * @param func Callback. + * @param immediateIfPossible If true and no refresh is in progress, the callback will + * be called immediately. + */ + virtual bool onNextRefresh(std::function const& func, + bool immediateIfPossible = true) = 0; + + /** + * @brief Add a new callback to be called when a new profile is created. + * + * Parameters of the callback: + * - The created profile (can be a temporary object, so it should not be stored). + * + * @param func Function to be called when a profile is created. + * + */ + virtual bool onProfileCreated(std::function const& func) = 0; + + /** + * @brief Add a new callback to be called when a profile is renamed. + * + * Parameters of the callback: + * - The renamed profile. + * - The old name of the profile. + * - The new name of the profile. + * + * @param func Function to be called when a profile is renamed. + * + */ + virtual bool onProfileRenamed( + std::function const& func) = 0; + + /** + * @brief Add a new callback to be called when a profile is removed. + * + * Parameters of the callback: + * - The name of the removed profile. + * + * The function is called after the profile has been removed, so the profile is not + * accessible anymore. + * + * @param func Function to be called when a profile is removed. + * + */ + virtual bool onProfileRemoved(std::function const& func) = 0; + + /** + * @brief Add a new callback to be called when the current profile is changed. + * + * Parameters of the callback: + * - The old profile. Can be a null pointer if no profile was set (e.g. at startup). + * - The new profile, cannot be null. + * + * The function is called when the profile is changed but some operations related to + * the profile might not be finished when this is called (e.g., the virtual file + * system might not be up-to-date). + * + * @param func Function to be called when the current profile change. + * + */ + virtual bool + onProfileChanged(std::function const& func) = 0; + + /** + * @brief Add a new callback to be called when a plugin setting is changed. + * + * Parameters of the callback: + * - Name of the plugin. + * - Name of the setting. + * - Old value of the setting. Can be a default-constructed (invalid) QVariant if + * the setting did not exist before. + * - New value of the setting. Can be a default-constructed (invalid) QVariant if + * the setting has been removed. + * + * @param func Function to be called when a plugin setting is changed. + */ + virtual bool onPluginSettingChanged( + std::function const& func) = 0; + + /** + * @brief Add a new callback to be called when a plugin is enabled. + * + * Parameters of the callback: + * - The enabled plugin. + * + * @param func Function to be called when a plugin is enabled. + */ + virtual bool onPluginEnabled(std::function const& func) = 0; + + /** + * @brief Add a new callback to be called when the specified plugin is enabled. + * + * @param name Name of the plugin to watch. + * @param func Function to be called when a plugin is enabled. + */ + virtual bool onPluginEnabled(const QString& pluginName, + std::function const& func) = 0; + + /** + * @brief Add a new callback to be called when a plugin is disabled. + * + * Parameters of the callback: + * - The disabled plugin. + * + * @param func Function to be called when a plugin is disabled. + */ + virtual bool onPluginDisabled(std::function const& func) = 0; + + /** + * @brief Add a new callback to be called when the specified plugin is disabled. + * + * @param name Name of the plugin to watch. + * @param func Function to be called when a plugin is disabled. + */ + virtual bool onPluginDisabled(const QString& pluginName, + std::function const& func) = 0; +}; + +} // namespace MOBase + +namespace MOBase::details +{ +// called from MO +QDLLEXPORT void setPluginDataPath(const QString& s); +} // namespace MOBase::details + +#endif // IMOINFO_H diff --git a/libs/uibase/include/uibase/iplugin.h b/libs/uibase/include/uibase/iplugin.h new file mode 100644 index 0000000..aae89dc --- /dev/null +++ b/libs/uibase/include/uibase/iplugin.h @@ -0,0 +1,146 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IPLUGIN_H +#define IPLUGIN_H + +#include "imoinfo.h" +#include "pluginrequirements.h" +#include "pluginsetting.h" +#include "versioninfo.h" +#include +#include +#include +#include + +namespace MOBase +{ + +class IPlugin +{ +public: + // For easier access in child class: + using Requirements = PluginRequirementFactory; + +public: + virtual ~IPlugin() {} + + /** + * @brief Initialize the plugin. + * + * Note that this function may never be called if no IOrganizer is available + * at that time, such as when creating the first instance in MO. For proxy + * plugins, init() is always called, but given a null IOrganizer. + * + * Plugins will probably want to store the organizer pointer. It is guaranteed + * to be valid as long as the plugin is loaded. + * + * These functions may be called before init(): + * - name() + * - see IPluginGame for more + * + * @return false if the plugin could not be initialized, true otherwise. + */ + virtual bool init(IOrganizer* organizer) = 0; + + /** + * this function may be called before init() + * + * @return the internal name of this plugin (used for example in the settings menu). + * + * @note Please ensure you use a name that will not change. Do NOT include a version + * number in the name. Do NOT use a localizable string (tr()) here. Settings for + * example are tied to this name, if you rename your plugin you lose settings users + * made. + */ + virtual QString name() const = 0; + + /** + * @return the localized name for this plugin. + * + * @note Unlike name(), this method can (and should!) return a localized name for the + * plugin. + * @note This method returns name() by default. + */ + virtual QString localizedName() const { return name(); } + + /** + * @brief Retrieve the name of the master plugin of this plugin. + * + * It is often easier to implement a functionality as multiple plugins in MO2, but + * ship the plugins together, e.g. as a Python module or using `createFunctions()`. In + * this case, having a master plugin (one of the plugin, or a separate one) tells MO2 + * that these plugins are linked and should also be displayed together in the UI. If + * MO2 ever implements automatic updates for plugins, the `master()` plugin will also + * be used for this purpose. + * + * @return the name of the master plugin of this plugin, or an empty string if this + * plugin does not have a master. + */ + virtual QString master() const { return ""; } + + /** + * @brief Retrieve the requirements for the plugins. + * + * This method is called right after init(). + * + * @return the requirements for this plugin. + */ + virtual std::vector> requirements() const + { + return {}; + } + + /** + * @return the author of this plugin. + */ + virtual QString author() const = 0; + + /** + * @return a short description of the plugin to be displayed to the user. + */ + virtual QString description() const = 0; + + /** + * @return the version of the plugin. This can be used to detect outdated versions of + * plugins. + */ + virtual VersionInfo version() const = 0; + + /** + * @return the list of configurable settings for this plugin (in the user interface). + * The list may be empty. + * + * @note Plugin can store "hidden" (from the user) settings using + * IOrganizer::persistent / IOrganizer::setPersistent. + */ + virtual QList settings() const = 0; + + /** + * @return whether the plugin should be enabled by default + */ + virtual bool enabledByDefault() const { return true; } +}; + +} // namespace MOBase + +Q_DECLARE_INTERFACE(MOBase::IPlugin, "com.tannin.ModOrganizer.Plugin/2.0") + +#endif // IPLUGIN_H diff --git a/libs/uibase/include/uibase/iplugindiagnose.h b/libs/uibase/include/uibase/iplugindiagnose.h new file mode 100644 index 0000000..5a9340c --- /dev/null +++ b/libs/uibase/include/uibase/iplugindiagnose.h @@ -0,0 +1,103 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2013 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 IPLUGINDIAGNOSE_H +#define IPLUGINDIAGNOSE_H + +#include +#include +#include +#include +#include + +namespace MOBase +{ + +/** + * @brief A plugin that creates problem reports to be displayed in the UI. + * This can be used to report problems related to the same plugin (which implements + * further interfaces) or as a stand-alone diagnosis tool. This does not derive from + * IPlugin to prevent multiple inheritance issues. For stand-alone diagnosis plugins, + * derive from IPlugin and IPluginDiagnose + */ +class IPluginDiagnose +{ +public: + /** + * @return a list of keys of active problems + * @note this is not expected to be called if isActive returns false + */ + virtual std::vector activeProblems() const = 0; + + /** + * @brief retrieve a short description for the specified problem for the overview + * page. HTML syntax is supported. + * @param key the identifier of the problem as reported by activeProblems() + * @return a (short) description text + * @throw should throw an exception if the key is not valid + */ + virtual QString shortDescription(unsigned int key) const = 0; + + /** + * @brief retrieve the full description for the specified problem. HTML syntax is + * supported. + * @param key the identifier of the problem as reported by activeProblems() + * @return a (long) description text + * @throw should throw an exception if the key is not valid + */ + virtual QString fullDescription(unsigned int key) const = 0; + + /** + * @param key the identifier of the problem as reported by activeProblems() + * @return true if this plugin provides a guide to fix the issue + * @throw should throw an exception if the key is not valid + */ + virtual bool hasGuidedFix(unsigned int key) const = 0; + + /** + * @brief start the guided fix for the specified problem + * @param key the identifier of the problem as reported by activeProblems() + * @throw should throw an exception if the key is not valid or if there is no guided + * fix for the issue + */ + virtual void startGuidedFix(unsigned int key) const = 0; + + /** + * @brief Register the callback to be called when this plugin is invalidated. + * + * Only one callback can be activate at a time. + */ + void onInvalidated(std::function callback) { m_OnInvalidated = callback; } + + virtual ~IPluginDiagnose() {} + +protected: + void invalidate() { m_OnInvalidated(); } + +private: + std::function m_OnInvalidated; +}; + +} // namespace MOBase + +Q_DECLARE_INTERFACE(MOBase::IPluginDiagnose, + "com.tannin.ModOrganizer.PluginDiagnose/1.1") + +#endif // IPLUGINDIAGNOSE_H diff --git a/libs/uibase/include/uibase/ipluginfilemapper.h b/libs/uibase/include/uibase/ipluginfilemapper.h new file mode 100644 index 0000000..350aa71 --- /dev/null +++ b/libs/uibase/include/uibase/ipluginfilemapper.h @@ -0,0 +1,49 @@ +/* +Mod Organizer shared UI functionality + +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 IPLUGINFILEMAPPER_H +#define IPLUGINFILEMAPPER_H + +#include "filemapping.h" +#include "iplugin.h" + +namespace MOBase +{ + +/** + * brief A plugin that adds virtual file links + * This does not derive from IPlugin to prevent multiple inheritance issues. + * For stand-alone mapping plugins, derive from IPlugin and IPluginDiagnose + */ +class IPluginFileMapper +{ +public: + /** + * @return a list of file maps + */ + virtual MappingType mappings() const = 0; +}; + +} // namespace MOBase + +Q_DECLARE_INTERFACE(MOBase::IPluginFileMapper, + "com.tannin.ModOrganizer.PluginFileMapper/2.0") + +#endif // IPLUGINDIAGNOSE_H diff --git a/libs/uibase/include/uibase/iplugingame.h b/libs/uibase/include/uibase/iplugingame.h new file mode 100644 index 0000000..0fa2508 --- /dev/null +++ b/libs/uibase/include/uibase/iplugingame.h @@ -0,0 +1,380 @@ +#ifndef IPLUGINGAME_H +#define IPLUGINGAME_H + +#include "executableinfo.h" +#include "iplugin.h" +#include "isavegame.h" + +class QIcon; +class QUrl; +class QString; + +#include +#include +#include +#include +#include +#include + +namespace MOBase +{ + +// Game plugins can be loaded without an IOrganizer being available, in which +// case detectGame() is called, but not init(). +// +// These functions may be called before init() (after detectGame()): +// - gameName() +// - isInstalled() +// - gameIcon() +// - gameDirectory() +// - dataDirectory() +// - gameVariants() +// - looksValid() +// - see IPlugin::init() for more +// +// +class IPluginGame : public QObject, public IPlugin +{ + Q_INTERFACES(IPlugin) + +public: + enum class LoadOrderMechanism + { + None, + FileTime, + PluginsTxt + }; + + enum class SortMechanism + { + NONE, + MLOX, + BOSS, + LOOT + }; + + enum ProfileSetting + { + MODS = 0x01, + CONFIGURATION = 0x02, + SAVEGAMES = 0x04, + PREFER_DEFAULTS = 0x08 + }; + + Q_DECLARE_FLAGS(ProfileSettings, ProfileSetting) + +public: + // Game plugin should not have requirements: + std::vector> + requirements() const final override + { + return {}; + } + + // Game plugin can not be disabled + bool enabledByDefault() const final override { return true; } + + /** + * this function may be called before init() + * + * @return name of the game + */ + virtual QString gameName() const = 0; + + /** + * used to override display-specific text in place of the gameName + * for example the text for the main window or the initial instance creation game + * plugin selection/list + * + * added for future translation purposes or to make plugins visually more accurate in + * the ui + * + * @return display-specific name of the game + */ + virtual QString displayGameName() const { return gameName(); } + + /** + * @brief Detect the game. + * + * This method is the first method called for game plugins (before init()). The + * following methods can be called after detectGame() but before init(): + * - gameName() + * - isInstalled() + * - gameIcon() + * - gameDirectory() + * - dataDirectory() + * - gameVariants() + * - looksValid() + * - see IPlugin::init() for more + */ + virtual void detectGame() = 0; + + /** + * @brief initialize a profile for this game + * @param directory the directory where the profile is to be initialized + * @param settings parameters for how the profile should be initialized + * @note the MO app does not yet support virtualizing only specific aspects but + * plugins should be written with this future functionality in mind + * @note this function will be used to initially create a profile, potentially to + * repair it or upgrade/downgrade it so the implementations have to gracefully handle + * the case that the directory already contains files! + */ + virtual void initializeProfile(const QDir& directory, + ProfileSettings settings) const = 0; + + /** + * @brief List save games in the specified folder. + * + * @param folder The folder containing the saves. + * + * @return the list of saves in the specified folder. + */ + /** + * @return file extension of save games for this game + */ + virtual std::vector> + listSaves(QDir folder) const = 0; + + /** + * this function may be called before init() + * + * @return true if this game has been discovered as installed, false otherwise + */ + virtual bool isInstalled() const = 0; + + /** + * this function may be called before init() + * + * @return an icon for this game + */ + virtual QIcon gameIcon() const = 0; + + /** + * this function may be called before init() + * + * @return directory to the game installation + */ + virtual QDir gameDirectory() const = 0; + + /** + * this function may be called before init() + * + * @return directory where the game expects to find its data files + */ + virtual QDir dataDirectory() const = 0; + + virtual QString modDataDirectory() const { return ""; } + + /** + * this function may be called before init() + * + * @return directories where we may find data files outside the main location + */ + virtual QMap secondaryDataDirectories() const { return {}; } + + /** + * @brief set the path to the managed game + * @param path to the game + * @note this will be called by by MO to set the concrete path of the game. This is + * particularly relevant if the path wasn't auto-detected but had to be set manually + * by the user + */ + virtual void setGamePath(const QString& path) = 0; + + /** + * @return directory of the documents folder where configuration files and such for + * this game reside + */ + virtual QDir documentsDirectory() const = 0; + + /** + * @return path to where save games are stored. + */ + virtual QDir savesDirectory() const = 0; + + /** + * @return list of automatically discovered executables of the game itself and tools + * surrounding it + */ + virtual QList executables() const { return {}; } + + /** + * @brief Get the default list of libraries that can be force loaded with executables + */ + virtual QList executableForcedLoads() const = 0; + + /** + * @return steam app id for this game. Should be empty for games not available on + * steam + * @note if a game is available in multiple versions those might have different app + * ids. the plugin should try to return the right one + */ + virtual QString steamAPPId() const { return ""; } + + /** + * @return list of plugins that are part of the game and not considered optional + */ + virtual QStringList primaryPlugins() const { return {}; } + + /** + * @return list of plugins enabled by the game but not in a strict load order + */ + virtual QStringList enabledPlugins() const { return {}; } + + /** + * this function may be called before init() + * + * @return list of game variants + * @note if there are multiple variants of a game (and the variants make a difference + * to the plugin) like a regular one and a GOTY-edition the plugin can return a list + * of them and the user gets to chose which one he owns. + */ + virtual QStringList gameVariants() const { return {}; } + + /** + * @brief if there are multiple game variants (returned by gameVariants) this will get + * called on start with the user-selected game edition + * @param variant the game edition selected by the user + */ + virtual void setGameVariant(const QString& variant) = 0; + + /** + * @brief Get the name of the executable that gets run + */ + virtual QString binaryName() const = 0; + + /** + * @brief Get the 'short' name of the game + * + * the short name of the game is used for - save ames, registry entries and + * nexus mod pages as far as I can see. + */ + virtual QString gameShortName() const = 0; + + /** + * @brief game name that's passed to the LOOT cli --game argument + * + * only applicable for games using LOOT based sorting + * defaults to gameShortName() + */ + virtual QString lootGameName() const { return gameShortName(); } + + /** + * @brief Get any primary alternative 'short' name for the game + * + * this is used to determine if a Nexus (or other) download source should be + * considered a 'primary' source for the game so that it isn't flagged as an + * alternative source + */ + virtual QStringList primarySources() const { return {}; } + + /** + * @brief Get any valid 'short' name for the game + * + * this is used to determine if a Nexus download is valid for the current game + * not all game variants have their own nexus pages and others can handle downloads + * from other nexus game pages and should be allowed + * + * the short name should be considered the primary handler for a directly supported + * game for puroses of auto-launching an instance + */ + virtual QStringList validShortNames() const { return {}; } + + /** + * @brief Get the 'short' name of the game + * + * the short name of the game is used for - save ames, registry entries and + * nexus mod pages as far as I can see. + */ + virtual QString gameNexusName() const { return ""; } + + /** + * @brief Get the list of .ini files this game uses + * + * @note It is important that the 'main' .ini file comes first in this list + */ + virtual QStringList iniFiles() const { return {}; } + + /** + * @brief Get a list of esp/esm files that are part of known dlcs + */ + virtual QStringList DLCPlugins() const { return {}; } + + /** + * @brief Get the current list of active Creation Club plugins + */ + virtual QStringList CCPlugins() const { return {}; } + + /* + * @brief determine the load order mechanism used by this game. + * + * @note this may throw an exception if the mechanism can't be determined + */ + virtual LoadOrderMechanism loadOrderMechanism() const + { + return LoadOrderMechanism::None; + } + + /** + * @brief determine the sorting mech + */ + virtual SortMechanism sortMechanism() const { return SortMechanism::NONE; } + + /** + * @brief Get the Nexus ID of Mod Organizer + */ + virtual int nexusModOrganizerID() const { return 0; } + + /** + * @brief Get the Nexus Game ID + */ + virtual int nexusGameID() const = 0; + + /** + * this function may be called before init() + * + * @brief See if the supplied directory looks like a valid game + */ + virtual bool looksValid(QDir const&) const = 0; + + /** + * @brief Get version of program + */ + virtual QString gameVersion() const = 0; + + /** + * @brief Get the name of the game launcher + */ + virtual QString getLauncherName() const = 0; + + /** + * @brief Get a URL for the support page for the game + */ + virtual QString getSupportURL() const { return ""; } + + /** + * @brief Gets a virtualization mapping for mod directories + * + * @note Maps internal mod directories to a list of paths + * @default Root directory maps to game data path(s) + */ + virtual QMap getModMappings() const + { + QMap map; + QStringList dataDirs = {dataDirectory().absolutePath()}; + for (auto path : secondaryDataDirectories()) { + dataDirs.append(path.absolutePath()); + } + map[""] = dataDirs; + return map; + } +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(IPluginGame::ProfileSettings) + +} // namespace MOBase + +Q_DECLARE_INTERFACE(MOBase::IPluginGame, "com.tannin.ModOrganizer.PluginGame/2.0") +Q_DECLARE_METATYPE(MOBase::IPluginGame const*) + +#endif // IPLUGINGAME_H diff --git a/libs/uibase/include/uibase/iplugingamefeatures.h b/libs/uibase/include/uibase/iplugingamefeatures.h new file mode 100644 index 0000000..553d302 --- /dev/null +++ b/libs/uibase/include/uibase/iplugingamefeatures.h @@ -0,0 +1,61 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2024 MO2 Team. 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 IPLUGINGAMEFEATURES_H +#define IPLUGINGAMEFEATURES_H + +namespace MOBase +{ + +class BSAInvalidation; +class DataArchives; +class GamePlugins; +class LocalSavegames; +class ModDataChecker; +class ModDataContent; +class SaveGameInfo; +class ScriptExtender; +class UnmanagedMods; + +/** + * @brief A plugin to implement game features. + * + */ +class IPluginGameFeautres +{ +public: + /** + * Retrieve the priority of these game features. + * + * For mergeable features, e.g., ModDataChecker, this indicates the order in which the + * features are merged, otherwise, it indicates which plugin should provide the + * feature. + * + * The managed game always has lowest priority. + * + * @return the priority of this set of features. + */ + virtual int priority() = 0; +}; + +} // namespace MOBase + +#endif // IPLUGINDIAGNOSE_H +#pragma once diff --git a/libs/uibase/include/uibase/iplugininstaller.h b/libs/uibase/include/uibase/iplugininstaller.h new file mode 100644 index 0000000..d033fe9 --- /dev/null +++ b/libs/uibase/include/uibase/iplugininstaller.h @@ -0,0 +1,157 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IPLUGININSTALLER_H +#define IPLUGININSTALLER_H + +#include + +#include "ifiletree.h" +#include "imodinterface.h" +#include "iplugin.h" + +namespace MOBase +{ + +class IInstallationManager; + +class IPluginInstaller : public IPlugin +{ + + Q_INTERFACES(IPlugin) + +public: + enum EInstallResult + { + RESULT_SUCCESS, + RESULT_FAILED, + RESULT_CANCELED, + RESULT_MANUALREQUESTED, + RESULT_NOTATTEMPTED, + RESULT_SUCCESSCANCEL, + RESULT_CATEGORYREQUESTED + }; + +public: + IPluginInstaller() : m_ParentWidget(nullptr), m_InstallationManager(nullptr) {} + + /** + * @brief Retrieve the priority of this installer. If multiple installers are able + * to handle an archive, the one with the highest priority wins. + * + * @return the priority of this installer. + */ + virtual unsigned int priority() const = 0; + + /** + * @return true if this plugin should be treated as a manual installer if the user + * explicitly requested one. A manual installer should offer the user maximum amount + * of customizability. + */ + virtual bool isManualInstaller() const = 0; + + /** + * @brief Method calls at the start of the installation process, before any other + * methods. This method is only called once per installation process, even for + * recursive installations (e.g. with the bundle installer). + * + * @param archive Path to the archive that is going to be installed. + * @param reinstallation True if this is a reinstallation, false otherwise. + * @param mod A currently installed mod corresponding to the archive being installed, + * or a null if there is no such mod. + * + * @note If `reinstallation` is true, then the given mod is the mod being reinstalled + * (the one selected by the user). If `reinstallation` is false and `currentMod` is + * not null, then it corresponds to a mod MO2 thinks corresponds to the archive (e.g. + * based on matching Nexus ID or name). + * @note The default implementation does nothing. + */ + virtual void onInstallationStart([[maybe_unused]] QString const& archive, + [[maybe_unused]] bool reinstallation, + [[maybe_unused]] IModInterface* currentMod) + {} + + /** + * @brief Method calls at the end of the installation process. This method is only + * called once per installation process, even for recursive installations (e.g. with + * the bundle installer). + * + * @param result The result of the installation. + * @param mod If the installation succeeded (result is RESULT_SUCCESS), contains the + * newly installed mod, otherwise it contains a null pointer. + * + * @note The default implementation does nothing. + */ + virtual void onInstallationEnd([[maybe_unused]] EInstallResult result, + [[maybe_unused]] IModInterface* newMod) + {} + + /** + * @brief Test if the archive represented by the tree parameter can be installed + * through this installer. + * + * @param tree a directory tree representing the archive. + * + * @return true if this installer can handle the archive. + */ + virtual bool isArchiveSupported(std::shared_ptr tree) const = 0; + + /** + * @brief Sets the widget that the tool should use as the parent whenever + * it creates a new modal dialog. + * + * @param widget The new parent widget. + */ + virtual void setParentWidget(QWidget* widget) { m_ParentWidget = widget; } + + /** + * @brief Sets the installation manager responsible for the installation process + * it can be used by plugins to access utility functions. + * + * @param manager The new installation manager. + */ + void setInstallationManager(IInstallationManager* manager) + { + m_InstallationManager = manager; + } + +protected: + /** + * @return the parent widget that the tool should use to create new dialogs and + * widgets. + */ + QWidget* parentWidget() const { return m_ParentWidget; } + + /** + * @return the manager responsible for the installation process. + */ + IInstallationManager* manager() const { return m_InstallationManager; } + +private: + QWidget* m_ParentWidget; + IInstallationManager* m_InstallationManager; +}; + +} // namespace MOBase + +Q_DECLARE_INTERFACE(MOBase::IPluginInstaller, + "com.tannin.ModOrganizer.PluginInstaller/1.0") + +#endif // IPLUGININSTALLER_H diff --git a/libs/uibase/include/uibase/iplugininstallercustom.h b/libs/uibase/include/uibase/iplugininstallercustom.h new file mode 100644 index 0000000..204639f --- /dev/null +++ b/libs/uibase/include/uibase/iplugininstallercustom.h @@ -0,0 +1,79 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IPLUGININSTALLERCUSTOM_H +#define IPLUGININSTALLERCUSTOM_H + +#include "guessedvalue.h" +#include "iplugininstaller.h" + +namespace MOBase +{ + +/** + * Custom installer for mods. Custom installers receive the archive name and have to go + * from there. They have to be able to extract the archive themself. + * Plugins implementing this interface have to contain the following line in the class + * declaration: Q_Interfaces(IPlugin IPluginInstaller IPluginInstallerCustom) + */ +class IPluginInstallerCustom : public QObject, public IPluginInstaller +{ + + Q_INTERFACES(IPluginInstaller) + +public: + /** + * @brief test if the archive represented by the file name can be installed through + * this installer + * @param filename of the archive + * @return true if this installer can handle the archive + * @note this is only called if the archive couldn't be opened by the caller, + * otherwise IPluginInstaller::isArchiveSupported(const DirectoryTree &tree) is called + */ + virtual bool isArchiveSupported(const QString& archiveName) const = 0; + + /** + * @return returns a list of file extensions that may be supported by this installer + */ + virtual std::set supportedExtensions() const = 0; + + /** + * install call + * @param modName name of the mod to install. As an input parameter this is the + * suggested name (i.e. from meta data) The installer may change this parameter to + * rename the mod) + * @param filename of the archive + * @param version version of the mod. May be empty if the version is not yet known. + * The plugin is responsible for setting the version on the created mod + * @param nexusID id of the mod or -1 if unknown. The plugin is responsible for + * setting the mod id for the created mod + * @return result of the installation process + */ + virtual EInstallResult install(GuessedValue& modName, QString gameName, + const QString& archiveName, const QString& version, + int nexusID) = 0; +}; + +} // namespace MOBase + +Q_DECLARE_INTERFACE(MOBase::IPluginInstallerCustom, + "com.tannin.ModOrganizer.PluginInstallerCustom/1.0") + +#endif // IPLUGININSTALLERCUSTOM_H diff --git a/libs/uibase/include/uibase/iplugininstallersimple.h b/libs/uibase/include/uibase/iplugininstallersimple.h new file mode 100644 index 0000000..a8795fc --- /dev/null +++ b/libs/uibase/include/uibase/iplugininstallersimple.h @@ -0,0 +1,69 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IPLUGININSTALLERSIMPLE_H +#define IPLUGININSTALLERSIMPLE_H + +#include "guessedvalue.h" +#include "iplugininstaller.h" + +namespace MOBase +{ + +/** + * Simple installer for mods. Simple installers only deal with an in-memory structure + * representing the archive and can modify what to install where by editing this + * structure. Actually extracting the archive is handled by the caller Plugins + * implementing this interface have to contain the following line in the class + * declaration: Q_Interfaces(IPlugin IPluginInstaller IPluginInstallerCustom) + */ +class IPluginInstallerSimple : public QObject, public IPluginInstaller +{ + + Q_INTERFACES(IPluginInstaller) + +public: + /** + * install call for the simple mode. The installer only needs to restructure the tree + * parameter, the caller does the rest + * @param modName name of the mod to install. As an input parameter this is the + * suggested name (i.e. from meta data) The installer may change this parameter to + * rename the mod) + * @param tree in-memory representation of the archive content + * @param version version of the mod. May be empty if the version is not yet known. + * Can be updated if the plugin can determine the version + * @param nexusID id of the mod or -1 if unknown. May be updated if the plugin can + * determine the mod id + * @return the result of the installation process. If "ERROR_NOTATTEMPTED" is + * returned, further installers will work with the modified tree. This may be useful + * when implementing a sort of filter, but usually tree should remain unchanged in + * that case. + */ + virtual EInstallResult install(GuessedValue& modName, + std::shared_ptr& tree, QString& version, + int& nexusID) = 0; +}; + +} // namespace MOBase + +Q_DECLARE_INTERFACE(MOBase::IPluginInstallerSimple, + "com.tannin.ModOrganizer.PluginInstallerSimple/1.0") + +#endif // IPLUGININSTALLERSIMPLE_H diff --git a/libs/uibase/include/uibase/ipluginlist.h b/libs/uibase/include/uibase/ipluginlist.h new file mode 100644 index 0000000..61f8f7d --- /dev/null +++ b/libs/uibase/include/uibase/ipluginlist.h @@ -0,0 +1,268 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2013 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 IPLUGINLIST_H +#define IPLUGINLIST_H + +#include + +class QString; + +#include +#include + +namespace MOBase +{ + +class IPluginList +{ + +public: + enum PluginState + { + STATE_MISSING, + STATE_INACTIVE, + STATE_ACTIVE + }; + + Q_DECLARE_FLAGS(PluginStates, PluginState) + +public: + virtual ~IPluginList() {} + + /** + * @return list of plugin names + */ + virtual QStringList pluginNames() const = 0; + + /** + * @brief retrieve the state of a plugin + * @param name filename of the plugin (without path but with file extension) + * @return one of the possible plugin states: missing, inactive or active + */ + virtual PluginStates state(const QString& name) const = 0; + + /** + * @brief set the state of a plugin + * @param name filename of the plugin (without path but with file extensions) + * @param state new state of the plugin. should be active or inactive + */ + virtual void setState(const QString& name, PluginStates state) = 0; + + /** + * @brief retrieve the priority of a plugin + * @param name filename of the plugin (without path but with file extension) + * @return the priority (the higher the more important). Returns -1 if the plugin + * doesn't exist + */ + virtual int priority(const QString& name) const = 0; + + /** + * @brief Change the priority of a plugin. + * + * @param name Filename of the plugin (without path but with file extension). + * @param newPriority New priority of the plugin. + * + * @return true on success, false if the priority change was not possible. This is + * usually because one of the parameters is invalid. The function returns true even if + * the plugin was not moved at the specified priority (e.g. when trying to move a + * non-master plugin before a master one). + */ + virtual bool setPriority(const QString& name, int newPriority) = 0; + + /** + * @brief retrieve the load order of a plugin + * @param name filename of the plugin (without path but with file extension) + * @return the load order of a plugin (the order in which the game loads it). If all + * plugins are enabled this is the same as the priority but disabled plugins will have + * a load order of -1. This also returns -1 if the plugin doesn't exist + */ + virtual int loadOrder(const QString& name) const = 0; + + /** + * @brief sets the load order of the plugin list. + * @param the new load order, specified by the list of plugin names, sorted. + * @note plugins not included in the list will be placed at highest priority + * in the order they were before + */ + virtual void setLoadOrder(const QStringList& pluginList) = 0; + + /** + * @brief determine if a plugin is a master file (basically a library, referenced by + * other plugins) + * @param name filename of the plugin (without path but with file extension) + * @return true if the file is a master, false if it isn't OR if the file doesn't + * exist. + * @note deprecated + */ + [[deprecated]] virtual bool isMaster(const QString& name) const = 0; + + /** + * @brief retrieve the list of masters required for this plugin + * @param name filename of the plugin (without path but with file extension) + * @return list of masters (filenames with extension, no path) + */ + virtual QStringList masters(const QString& name) const = 0; + + /** + * @brief retrieve the name of the origin of a plugin. This is either the (internal!) + * name of a mod or "overwrite" or "data" + * @param name filename of the plugin (without path but with file extension) + * @return name of the origin or an empty string if the plugin doesn't exist + * @note the internal name of a mod can differ from the display name for + * disambiguation + */ + virtual QString origin(const QString& name) const = 0; + + /** + * @brief invoked whenever the application felt it necessary to refresh the list (i.e. + * because of external changes) + */ + virtual bool onRefreshed(const std::function& callback) = 0; + + /** + * @brief invoked whenever a plugin has changed priority + */ + virtual bool + onPluginMoved(const std::function& func) = 0; + + /** + * @brief Installs a handler for the event that the state of plugin changed + * (active/inactive). + * + * Parameters of the callback: + * - Map containing the plugins whose states have changed. Keys are plugin names and + * values are mod states. + * + * @param func The signal to be called when the states of any plugins change. + * + * @return true if the handler was successfully installed, false otherwise (there is + * as of now no known reason this should fail). + */ + virtual bool onPluginStateChanged( + const std::function&)>& func) = 0; + + /** + * @brief determine if a plugin has the .esm extension (basically a library, + * referenced by other plugins) + * @param name filename of the plugin (without path but with file extension) + * @return true if the file has the .esm extension, false if it isn't OR if the file + * doesn't exist. + * @note in gamebryo games, a master file will usually have a .esm file + * extension but technically an esp can be flagged as master and an esm might + * not be + */ + virtual bool hasMasterExtension(const QString& name) const = 0; + + /** + * @brief determine if a plugin has the .esl extension + * @param name filename of the plugin (without path but with file extension) + * @return true if the file has the .esl extension, false if it isn't OR if the file + * doesn't exist. + * @note in gamebryo games, a light file will usually have a .esl file + * extension but technically an esp can be flagged as light and an esm might + * not be + */ + virtual bool hasLightExtension(const QString& name) const = 0; + + /** + * @brief determine if a plugin is flagged as master (basically a library, referenced + * by other plugins) + * @param name filename of the plugin (without path but with file extension) + * @return true if the file is flagged as master, false if it isn't OR if the file + * doesn't exist. + * @note in gamebryo games, a master file will usually have a .esm file + * extension but technically an esp can be flagged as master and an esm might + * not be + */ + virtual bool isMasterFlagged(const QString& name) const = 0; + + /** + * @brief determine if a plugin is flagged as medium + * @param name filename of the plugin (without path but with file extension) + * @return true if the file is flagged as medium, false if it isn't OR if the file + * doesn't exist. + * @note this plugin flag was added in Starfield and signifies plugins in between + * master and light plugins. + */ + virtual bool isMediumFlagged(const QString& name) const = 0; + + /** + * @brief determine if a plugin is flagged as light + * @param name filename of the plugin (without path but with file extension) + * @return true if the file is flagged as light, false if it isn't OR if the file + * doesn't exist. + * @note in gamebryo games, a light file will usually have a .esl file + */ + virtual bool isLightFlagged(const QString& name) const = 0; + + /** + * @brief determine if a plugin is flagged as blueprint + * @param name filename of the plugin (without path but with file extension) + * @return true if the file is flagged as blueprint, false if it isn't OR if the file + * doesn't exist. + * @note this plugin flag was added in Starfield and signifies plugins that are + * hidden in the Creation Kit and removed from Plugins.txt when the game loads a save + */ + virtual bool isBlueprintFlagged(const QString& name) const = 0; + + /** + * @brief determine if a plugin has no records + * @param name filename of the plugin (without path but with file extension) + * @return true if the file has no records, false if it does OR if the file doesn't + * exist. + */ + virtual bool hasNoRecords(const QString& name) const = 0; + + /** + * @brief retrieve the form version of a plugin + * @param name filename of the plugin (without path but with file extension) + * @return the form version of the plugin, 0 if it doesn't have a form version or -1 + * if the plugin doesn't exist + * @note Oblivion-style plugin headers don't have a form version + */ + virtual int formVersion(const QString& name) const = 0; + + /** + * @brief retrieve the header version of a plugin + * @param name filename of the plugin (without path but with file extension) + * @return the header version of the plugin or -1 if the plugin doesn't exist + */ + virtual float headerVersion(const QString& name) const = 0; + + /** + * @brief retrieve the author of a plugin + * @param name filename of the plugin (without path but with file extension) + * @return the author of the plugin or an empty string if the plugin doesn't exist + */ + virtual QString author(const QString& name) const = 0; + + /** + * @brief retrieve the description of a plugin + * @param name filename of the plugin (without path but with file extension) + * @return the description of the plugin or an empty string if the plugin doesn't + * exist + */ + virtual QString description(const QString& name) const = 0; +}; + +} // namespace MOBase + +#endif // IPLUGINLIST_H diff --git a/libs/uibase/include/uibase/ipluginmodpage.h b/libs/uibase/include/uibase/ipluginmodpage.h new file mode 100644 index 0000000..78630d9 --- /dev/null +++ b/libs/uibase/include/uibase/ipluginmodpage.h @@ -0,0 +1,92 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2013 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 IPLUGINMODPAGE_H +#define IPLUGINMODPAGE_H + +#include "iplugin.h" +#include "modrepositoryfileinfo.h" +#include + +namespace MOBase +{ + +class IPluginModPage : public QObject, public IPlugin +{ + Q_INTERFACES(IPlugin) +public: + /** + * @return name of the Page as displayed in the ui + */ + virtual QString displayName() const = 0; + + /** + * @return icon to be displayed with the page + */ + virtual QIcon icon() const = 0; + + /** + * @return url to open when the user wants to visit this mod page + */ + virtual QUrl pageURL() const = 0; + + /** + * @return true if the page should be opened in the integrated browser + * @note unless the page provides a special means of starting downloads (like the + * nxm:// url schema on nexus) it will not be possible to handle downloads unless the + * integrated browser is used! + */ + virtual bool useIntegratedBrowser() const = 0; + + /** + * @brief test if the plugin handles a download + * @param pageURL url of the page that contained the donwload link + * @param downloadURL the actual download link + * @param fileInfo if the plugin can derive information from the urls it can store + * them here and they will be passed along with the download and be made available on + * installation + * @return true if this plugin wants to handle the specified download + */ + virtual bool handlesDownload(const QUrl& pageURL, const QUrl& downloadURL, + ModRepositoryFileInfo& fileInfo) const = 0; + + /** + * @brief sets the widget that the tool should use as the parent whenever + * it creates a new modal dialog + * @param widget the new parent widget + */ + virtual void setParentWidget(QWidget* widget) { m_ParentWidget = widget; } + +protected: + /** + * @brief getter for the parent widget + * @return parent widget + */ + QWidget* parentWidget() const { return m_ParentWidget; } + +private: + QWidget* m_ParentWidget; +}; + +} // namespace MOBase + +Q_DECLARE_INTERFACE(MOBase::IPluginModPage, "com.tannin.ModOrganizer.PluginModPage/1.0") + +#endif // IPLUGINMODPAGE_H diff --git a/libs/uibase/include/uibase/ipluginpreview.h b/libs/uibase/include/uibase/ipluginpreview.h new file mode 100644 index 0000000..47ace79 --- /dev/null +++ b/libs/uibase/include/uibase/ipluginpreview.h @@ -0,0 +1,72 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2014 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 IPLUGINPREVIEW_H +#define IPLUGINPREVIEW_H + +#include "iplugin.h" + +namespace MOBase +{ + +class IPluginPreview : public QObject, public IPlugin +{ + Q_INTERFACES(IPlugin) +public: + /** + * @return returns a set of file extensions that may be supported + */ + virtual std::set supportedExtensions() const = 0; + + /** + * @return return whether or not the preview supports raw data previews (likely + * sourced from an archive) + */ + virtual bool supportsArchives() const { return false; } + + /** + * @brief generate a preview widget for the specified file + * @param fileName name of the file to preview + * @param maxSize maximum size of the generated widget + * @return a widget showing the file + */ + virtual QWidget* genFilePreview(const QString& fileName, + const QSize& maxSize) const = 0; + + /** + * @brief generate a preview widget from an archive file loaded into memory + * @param fileData data of file to preview + * @param fileName path of file in virtual filesystem + * @param maxSize maxiumum size of the generated widget + */ + virtual QWidget* genDataPreview(const QByteArray& fileData, const QString& fileName, + const QSize& maxSize) const + { + return nullptr; + } + +private: +}; + +} // namespace MOBase + +Q_DECLARE_INTERFACE(MOBase::IPluginPreview, "com.tannin.ModOrganizer.PluginPreview/1.0") + +#endif // IPLUGINPREVIEW_H diff --git a/libs/uibase/include/uibase/ipluginproxy.h b/libs/uibase/include/uibase/ipluginproxy.h new file mode 100644 index 0000000..b0b8d84 --- /dev/null +++ b/libs/uibase/include/uibase/ipluginproxy.h @@ -0,0 +1,83 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IPLUGINPROXY_H +#define IPLUGINPROXY_H + +#include +#include +#include + +#include "iplugin.h" + +namespace MOBase +{ + +class IPluginProxy : public IPlugin +{ +public: + IPluginProxy() : m_ParentWidget(nullptr) {} + + /** + * @brief List the plugins managed by this proxy in the given + * folder. + * + * @param pluginPath Path containing the plugins. + * + * @return list of plugin identifiers that supported by this proxy. + */ + virtual QStringList pluginList(const QDir& pluginPath) const = 0; + + /** + * @brief Load the plugins corresponding to the given identifier. + * + * @param identifier Identifier of the proxied plugin to load. + * + * @return a list of QObject, one for each plugins in the given identifier. + */ + virtual QList load(const QString& identifier) = 0; + + /** + * @brief Unload the plugins corresponding to the given identifier. + * + * @param identifier Identifier of the proxied plugin to unload. + */ + virtual void unload(const QString& identifier) = 0; + + /** + * @brief Sets the widget that the tool should use as the parent whenever + * it creates a new modal dialog. + * + * @param widget The new parent widget. + */ + void setParentWidget(QWidget* widget) { m_ParentWidget = widget; } + +protected: + QWidget* parentWidget() const { return m_ParentWidget; } + +private: + QWidget* m_ParentWidget; +}; + +} // namespace MOBase + +Q_DECLARE_INTERFACE(MOBase::IPluginProxy, "com.tannin.ModOrganizer.PluginProxy/1.0") + +#endif // IPLUGINPROXY_H diff --git a/libs/uibase/include/uibase/iplugintool.h b/libs/uibase/include/uibase/iplugintool.h new file mode 100644 index 0000000..303ab36 --- /dev/null +++ b/libs/uibase/include/uibase/iplugintool.h @@ -0,0 +1,87 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 IPLUGINTOOL_H +#define IPLUGINTOOL_H + +#include "iplugin.h" +#include + +namespace MOBase +{ + +class IPluginTool : public QObject, public virtual IPlugin +{ + Q_INTERFACES(IPlugin) +public: + IPluginTool() : m_ParentWidget(nullptr) {} + + /** + * @return name of the tool as displayed in the ui + */ + virtual QString displayName() const = 0; + + /** + * @brief For IPluginTool, this returns displayName(). + * + */ + virtual QString localizedName() const { return displayName(); } + + /** + * @return tooltip string + */ + + virtual QString tooltip() const = 0; + /** + * @return icon to be displayed with the tool + */ + virtual QIcon icon() const = 0; + + /** + * @brief sets the widget that the tool should use as the parent whenever + * it creates a new modal dialog + * @param widget the new parent widget + */ + virtual void setParentWidget(QWidget* widget) { m_ParentWidget = widget; } + +public Q_SLOTS: + + /** + * @brief called when the user clicks to start the tool. + * @note This must not throw an exception! + */ + virtual void display() const = 0; + +protected: + /** + * @brief getter for the parent widget + * @return parent widget + */ + QWidget* parentWidget() const { return m_ParentWidget; } + +private: + QWidget* m_ParentWidget; +}; + +} // namespace MOBase + +Q_DECLARE_INTERFACE(MOBase::IPluginTool, "com.tannin.ModOrganizer.PluginTool/1.0") + +#endif // IPLUGINTOOL_H diff --git a/libs/uibase/include/uibase/iprofile.h b/libs/uibase/include/uibase/iprofile.h new file mode 100644 index 0000000..1706e3e --- /dev/null +++ b/libs/uibase/include/uibase/iprofile.h @@ -0,0 +1,41 @@ +#ifndef IPROFILE +#define IPROFILE + +#include +#include + +namespace MOBase +{ + +class IProfile +{ + +public: + virtual ~IProfile() {} + + virtual QString name() const = 0; + virtual QString absolutePath() const = 0; + virtual bool localSavesEnabled() const = 0; + virtual bool localSettingsEnabled() const = 0; + virtual bool invalidationActive(bool* supported) const = 0; + + /** + * @brief Retrieve the absolute file path to the corresponding INI file for this + * profile. + * + * @param iniFile INI file to retrieve a path for. This can either be the + * name of a file or a path to the absolute file outside of the profile. + * + * @return the absolute path for the given INI file for this profile. + * + * @note If iniFile does not correspond to a file in the list of INI files for the + * current game (as returned by IPluginGame::iniFiles), the path to the global + * file will be returned (if iniFile is absolute, iniFile is returned, otherwiise + the path is assumed relative to the game documents directory). + */ + virtual QString absoluteIniFilePath(QString iniFile) const = 0; +}; + +} // namespace MOBase + +#endif // IPROFILE diff --git a/libs/uibase/include/uibase/isavegame.h b/libs/uibase/include/uibase/isavegame.h new file mode 100644 index 0000000..2f69446 --- /dev/null +++ b/libs/uibase/include/uibase/isavegame.h @@ -0,0 +1,58 @@ +#ifndef ISAVEGAMEINFO_H +#define ISAVEGAMEINFO_H + +#include + +class QString; +class QDateTime; + +namespace MOBase +{ + +/** Base class for information about what is in a save game */ +class ISaveGame +{ +public: + virtual ~ISaveGame() {} + + /** + * @return the path of the (main) save file, either as an absolute file or + * relative to the save folder for which this save was created. + */ + virtual QString getFilepath() const = 0; + + /** + * @brief Retrieve the creation time of the save. + * + * Note that this might not be the same as the creation time of the file. + */ + virtual QDateTime getCreationTime() const = 0; + + /** + * @brief Retrieve the name of this save. + * + * @return the name of this save. + */ + virtual QString getName() const = 0; + + /** + * @brief Get a name which can be used to identify sets of saves. + * + * This is usually the PC name for RPG games. The name can contain '/' that + * are considered separate section for better visualization (not yet implemented). + */ + virtual QString getSaveGroupIdentifier() const = 0; + + /** + * @brief Gets all the files related to this save + * + * Note: This must return the actual list, not the potential list. + */ + virtual QStringList allFiles() const = 0; +}; + +} // namespace MOBase + +Q_DECLARE_METATYPE(MOBase::ISaveGame const*) + +#endif // SAVEGAMEINFO_H diff --git a/libs/uibase/include/uibase/isavegameinfowidget.h b/libs/uibase/include/uibase/isavegameinfowidget.h new file mode 100644 index 0000000..d0620b0 --- /dev/null +++ b/libs/uibase/include/uibase/isavegameinfowidget.h @@ -0,0 +1,30 @@ +#ifndef ISAVEGAMEINFOWIDGET_H +#define ISAVEGAMEINFOWIDGET_H + +#include + +#include "isavegame.h" + +class QFile; + +namespace MOBase +{ + +/** Base class for a save game info widget. + * + * This supports something or other + */ +class ISaveGameInfoWidget : public QWidget +{ +public: + ISaveGameInfoWidget(QWidget* parent = 0) : QWidget(parent) {} + + virtual ~ISaveGameInfoWidget() {} + + /** Set the save file to display in the widget */ + virtual void setSave(ISaveGame const&) = 0; +}; + +} // namespace MOBase + +#endif // ISAVEGAMEINFOWIDGET_H diff --git a/libs/uibase/include/uibase/json.h b/libs/uibase/include/uibase/json.h new file mode 100644 index 0000000..ad41bbc --- /dev/null +++ b/libs/uibase/include/uibase/json.h @@ -0,0 +1,95 @@ +/** + * QtJson - A simple class for parsing JSON data into a QVariant hierarchies and + * vice-versa. Copyright (C) 2011 Eeli Reilin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file json.h + */ + +#ifndef JSON_H +#define JSON_H + +#include +#include +#include + +/** + * \namespace QtJson + * \brief A JSON data parser + * + * Json parses a JSON data into a QVariant hierarchy. + */ +namespace QtJson +{ +typedef QVariantMap JsonObject; +typedef QVariantList JsonArray; + +/** + * Parse a JSON string + * + * \param json The JSON data + */ +QVariant parse(const QString& json); + +/** + * Parse a JSON string + * + * \param json The JSON data + * \param success The success of the parsing + */ +QVariant parse(const QString& json, bool& success); + +/** + * This method generates a textual JSON representation + * + * \param data The JSON data generated by the parser. + * + * \return QByteArray Textual JSON representation in UTF-8 + */ +QByteArray serialize(const QVariant& data); + +/** + * This method generates a textual JSON representation + * + * \param data The JSON data generated by the parser. + * \param success The success of the serialization + * + * \return QByteArray Textual JSON representation in UTF-8 + */ +QByteArray serialize(const QVariant& data, bool& success); + +/** + * This method generates a textual JSON representation + * + * \param data The JSON data generated by the parser. + * + * \return QString Textual JSON representation + */ +QString serializeStr(const QVariant& data); + +/** + * This method generates a textual JSON representation + * + * \param data The JSON data generated by the parser. + * \param success The success of the serialization + * + * \return QString Textual JSON representation + */ +QString serializeStr(const QVariant& data, bool& success); +} // namespace QtJson + +#endif // JSON_H diff --git a/libs/uibase/include/uibase/lineeditclear.h b/libs/uibase/include/uibase/lineeditclear.h new file mode 100644 index 0000000..dfded64 --- /dev/null +++ b/libs/uibase/include/uibase/lineeditclear.h @@ -0,0 +1,40 @@ +/**************************************************************************** +** +** Copyright (c) 2007 Trolltech ASA +** +** Use, modification and distribution is allowed without limitation, +** warranty, liability or support of any kind. +** +****************************************************************************/ + +#ifndef LINEEDITCLEAR_H +#define LINEEDITCLEAR_H + +#include "dllimport.h" +#include + +class QToolButton; + +namespace MOBase +{ + +class QDLLEXPORT LineEditClear : public QLineEdit +{ + Q_OBJECT + +public: + LineEditClear(QWidget* parent = 0); + +protected: + void resizeEvent(QResizeEvent*); + +private slots: + void updateCloseButton(const QString& text); + +private: + QToolButton* clearButton; +}; + +} // namespace MOBase + +#endif // LINEEDITCLEAR_H diff --git a/libs/uibase/include/uibase/linklabel.h b/libs/uibase/include/uibase/linklabel.h new file mode 100644 index 0000000..80fa958 --- /dev/null +++ b/libs/uibase/include/uibase/linklabel.h @@ -0,0 +1,38 @@ +#ifndef UIBASE_LINKLABEL_INCLUDED +#define UIBASE_LINKLABEL_INCLUDED + +#include "dllimport.h" +#include + +// this is a hack to allow .qss files to change the link color +// +// there's nothing in qt to change link colors from a qss file and the color +// can't even be changed on individual widgets, they all use the _global_ +// palette on qApp +// +// so as soon as there's a LinkLabel present on screen, the global link color +// will be set to whatever's in the qss file for: +// +// LinkLabel { qproperty-linkColor: cssColor; } +// +// this doesn't work for links that are visible, so changing the qss live won't +// change the colors for those, MO has to be restarted +// +// apart from that, `LinkLabel` is just a `QLabel` +// +class QDLLEXPORT LinkLabel : public QLabel +{ + Q_OBJECT; + Q_PROPERTY(QColor linkColor READ linkColor WRITE setLinkColor); + +public: + LinkLabel(QWidget* parent = nullptr); + + QColor linkColor() const; + void setLinkColor(const QColor& c); + +private: + static QColor m_linkColor; +}; + +#endif // UIBASE_LINKLABEL_INCLUDED diff --git a/libs/uibase/include/uibase/log.h b/libs/uibase/include/uibase/log.h new file mode 100644 index 0000000..1049506 --- /dev/null +++ b/libs/uibase/include/uibase/log.h @@ -0,0 +1,346 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "dllimport.h" +#include "formatters.h" +#include + +namespace spdlog +{ +class logger; +} +namespace spdlog::sinks +{ +class sink; +} + +namespace MOBase::log +{ + +enum Levels +{ + Debug = 0, + Info = 1, + Warning = 2, + Error = 3 +}; + +struct BlacklistEntry +{ + std::string filter; + std::string replacement; +}; + +} // namespace MOBase::log + +namespace MOBase::log::details +{ + +// TODO: remove for C++23 +template +concept formattable = requires(T& v, std::format_context ctx) { + std::formatter>().format(v, ctx); +}; + +template +concept RuntimeFormatString = requires(F&& f, Args&&... args) { + (formattable && ...); + !std::is_convertible_v, std::string_view>; +}; + +void QDLLEXPORT doLogImpl(spdlog::logger& lg, Levels lv, const std::string& s) noexcept; + +template +void doLog(spdlog::logger& logger, Levels lv, + const std::vector bl, + std::format_string format, Args&&... args) noexcept +{ + // format errors are logged without much information to avoid throwing again + + std::string s; + try { + s = std::format(format, std::forward(args)...); + + // check the blacklist + for (const BlacklistEntry& entry : bl) { + MOBase::ireplace_all(s, entry.filter, entry.replacement); + } + } catch (std::format_error&) { + s = "format error while logging"; + lv = Levels::Error; + } catch (std::exception&) { + s = "exception while formatting for logging"; + lv = Levels::Error; + } catch (...) { + s = "unknown exception while formatting for logging"; + lv = Levels::Error; + } + + doLogImpl(logger, lv, s); +} + +template +void doLog(spdlog::logger& logger, Levels lv, + const std::vector bl, F&& format, + Args&&... args) noexcept +{ + std::string s; + + // format errors are logged without much information to avoid throwing again + + try { + if constexpr (sizeof...(Args) == 0) { + s = std::format("{}", std::forward(format)); + } else if constexpr (std::is_same_v, QString>) { + s = std::vformat(format.toStdString(), std::make_format_args(args...)); + } else { + s = std::vformat(std::forward(format), std::make_format_args(args...)); + } + + // check the blacklist + for (const BlacklistEntry& entry : bl) { + MOBase::ireplace_all(s, entry.filter, entry.replacement); + } + } catch (std::format_error&) { + s = "format error while logging"; + lv = Levels::Error; + } catch (std::exception&) { + s = "exception while formatting for logging"; + lv = Levels::Error; + } catch (...) { + s = "unknown exception while formatting for logging"; + lv = Levels::Error; + } + + doLogImpl(logger, lv, s); +} + +} // namespace MOBase::log::details + +namespace MOBase::log +{ + +struct QDLLEXPORT File +{ +public: + enum Types + { + None = 0, + Daily, + Rotating, + Single + }; + + File(); + + static File daily(std::filesystem::path file, int hour, int minute); + + static File rotating(std::filesystem::path file, std::size_t maxSize, + std::size_t maxFiles); + + static File single(std::filesystem::path file); + + Types type; + std::filesystem::path file; + std::size_t maxSize, maxFiles; + int dailyHour, dailyMinute; +}; + +struct Entry +{ + std::chrono::system_clock::time_point time; + Levels level; + std::string message; + std::string formattedMessage; +}; + +using Callback = void(Entry); + +struct LoggerConfiguration +{ + std::string name; + Levels maxLevel = Levels::Info; + std::string pattern; + bool utc = false; + std::vector blacklist; +}; + +class QDLLEXPORT Logger +{ +public: + Logger(LoggerConfiguration conf); + ~Logger(); + + Levels level() const; + void setLevel(Levels lv); + + void setPattern(const std::string& pattern); + void setFile(const File& f); + void setCallback(Callback* f); + + void addToBlacklist(const std::string& filter, const std::string& replacement); + void removeFromBlacklist(const std::string& filter); + void resetBlacklist(); + + template + requires(details::RuntimeFormatString) + void debug(F&& format, Args&&... args) noexcept + { + log(Debug, std::forward(format), std::forward(args)...); + } + + template + void debug(std::format_string format, Args&&... args) noexcept + { + log(Debug, format, std::forward(args)...); + } + + template + requires(details::RuntimeFormatString) + void info(F&& format, Args&&... args) noexcept + { + log(Info, std::forward(format), std::forward(args)...); + } + + template + void info(std::format_string format, Args&&... args) noexcept + { + log(Info, format, std::forward(args)...); + } + + template + requires(details::RuntimeFormatString) + void warn(F&& format, Args&&... args) noexcept + { + log(Warning, std::forward(format), std::forward(args)...); + } + + template + void warn(std::format_string format, Args&&... args) noexcept + { + log(Warning, format, std::forward(args)...); + } + + template + requires(details::RuntimeFormatString) + void error(F&& format, Args&&... args) noexcept + { + log(Error, std::forward(format), std::forward(args)...); + } + + template + void error(std::format_string format, Args&&... args) noexcept + { + log(Error, format, std::forward(args)...); + } + + template + requires(details::RuntimeFormatString) + void log(Levels lv, F&& format, Args&&... args) noexcept + { + details::doLog(*m_logger, lv, m_conf.blacklist, std::forward(format), + std::forward(args)...); + } + + template + void log(Levels lv, std::format_string format, Args&&... args) noexcept + { + details::doLog(*m_logger, lv, m_conf.blacklist, format, + std::forward(args)...); + } + +private: + LoggerConfiguration m_conf; + std::unique_ptr m_logger; + std::shared_ptr m_sinks; + std::shared_ptr m_console, m_callback, m_file; + + void createLogger(const std::string& name); + void addSink(std::shared_ptr sink); +}; + +QDLLEXPORT void createDefault(LoggerConfiguration conf); +QDLLEXPORT Logger& getDefault(); + +template + requires(details::RuntimeFormatString) +void debug(F&& format, Args&&... args) noexcept +{ + getDefault().debug(std::forward(format), std::forward(args)...); +} + +template +void debug(std::format_string format, Args&&... args) noexcept +{ + getDefault().debug(format, std::forward(args)...); +} + +template + requires(details::RuntimeFormatString) +void info(F&& format, Args&&... args) noexcept +{ + getDefault().info(std::forward(format), std::forward(args)...); +} + +template +void info(std::format_string format, Args&&... args) noexcept +{ + getDefault().info(format, std::forward(args)...); +} + +template + requires(details::RuntimeFormatString) +void warn(F&& format, Args&&... args) noexcept +{ + getDefault().warn(std::forward(format), std::forward(args)...); +} + +template +void warn(std::format_string format, Args&&... args) noexcept +{ + getDefault().warn(format, std::forward(args)...); +} + +template + requires(details::RuntimeFormatString) +void error(F&& format, Args&&... args) noexcept +{ + getDefault().error(std::forward(format), std::forward(args)...); +} + +template +void error(std::format_string format, Args&&... args) noexcept +{ + getDefault().error(format, std::forward(args)...); +} + +template + requires(details::RuntimeFormatString) +void log(Levels lv, F&& format, Args&&... args) noexcept +{ + getDefault().log(lv, std::forward(format), std::forward(args)...); +} + +template +void log(Levels lv, std::format_string format, Args&&... args) noexcept +{ + getDefault().log(lv, format, std::forward(args)...); +} + +// +QDLLEXPORT QString levelToString(Levels level); + +} // namespace MOBase::log diff --git a/libs/uibase/include/uibase/memoizedlock.h b/libs/uibase/include/uibase/memoizedlock.h new file mode 100644 index 0000000..1888484 --- /dev/null +++ b/libs/uibase/include/uibase/memoizedlock.h @@ -0,0 +1,61 @@ +#ifndef MO_UIBASE_MEMOIZEDLOCK_INCLUDED +#define MO_UIBASE_MEMOIZEDLOCK_INCLUDED + +// Do not put this in utility.h otherwise the C# projects will +// fail to compile since apparently is not available in +// C++/CLI projects. + +#include +#include + +namespace MOBase +{ + +/** + * Class that can be used to perform thread-safe memoization. + * + * Each instance hold a flag indicating if the current value is up-to-date + * or not. This flag can be reset using `invalidate()`. When the value is queried, + * the flag is checked, and if it is not up-to-date, the given callback is used + * to compute the value. + * + * The computation and update of the value is locked to avoid concurrent modifications. + * + * @tparam T Type of value ot memoized. + * @tparam Fn Type of the callback. + */ +template > +class MemoizedLocked +{ +public: + template + MemoizedLocked(Callable&& callable, T value = {}) + : m_Fn{std::forward(callable)}, m_Value{std::move(value)} + {} + + template + T& value(Args&&... args) const + { + if (m_NeedUpdating) { + std::scoped_lock lock(m_Mutex); + if (m_NeedUpdating) { + m_Value = std::invoke(m_Fn, std::forward(args)...); + m_NeedUpdating = false; + } + } + return m_Value; + } + + void invalidate() { m_NeedUpdating = true; } + +private: + mutable std::mutex m_Mutex; + mutable std::atomic m_NeedUpdating{true}; + + Fn m_Fn; + mutable T m_Value; +}; + +} // namespace MOBase + +#endif diff --git a/libs/uibase/include/uibase/moassert.h b/libs/uibase/include/uibase/moassert.h new file mode 100644 index 0000000..492381c --- /dev/null +++ b/libs/uibase/include/uibase/moassert.h @@ -0,0 +1,36 @@ +#ifndef UIBASE_MOASSERT_INCLUDED +#define UIBASE_MOASSERT_INCLUDED + +#include "log.h" +#include + +namespace MOBase +{ + +template +inline void MOAssert(T&& t, const char* exp, const char* file, int line, + const char* func) +{ + if (!t) { + log::error("assertion failed: {}:{} {}: '{}'", file, line, func, exp); + +#ifdef _WIN32 + if (IsDebuggerPresent()) { + DebugBreak(); + } +#else + // On Linux, raise SIGTRAP if a debugger might be attached + raise(SIGTRAP); +#endif + } +} + +} // namespace MOBase + +#ifdef _MSC_VER +#define MO_ASSERT(v) MOBase::MOAssert(v, #v, __FILE__, __LINE__, __FUNCSIG__) +#else +#define MO_ASSERT(v) MOBase::MOAssert(v, #v, __FILE__, __LINE__, __PRETTY_FUNCTION__) +#endif + +#endif // UIBASE_MOASSERT_INCLUDED diff --git a/libs/uibase/include/uibase/modrepositoryfileinfo.h b/libs/uibase/include/uibase/modrepositoryfileinfo.h new file mode 100644 index 0000000..bf41f72 --- /dev/null +++ b/libs/uibase/include/uibase/modrepositoryfileinfo.h @@ -0,0 +1,56 @@ +#ifndef MODREPOSITORYFILEINFO_H +#define MODREPOSITORYFILEINFO_H + +#include "versioninfo.h" +#include +#include +#include + +namespace MOBase +{ +enum EFileCategory +{ + TYPE_UNKNOWN = 0, + TYPE_MAIN, + TYPE_UPDATE, + TYPE_OPTION +}; + +class QDLLEXPORT ModRepositoryFileInfo : public QObject +{ + Q_OBJECT + +public: + ModRepositoryFileInfo(const ModRepositoryFileInfo& reference); + ModRepositoryFileInfo(QString gameName = "", int modID = 0, int fileID = 0); + QString toString() const; + + static ModRepositoryFileInfo createFromJson(const QString& data); + QString name; + QString uri; + QString description; + VersionInfo version; + VersionInfo newestVersion; + int categoryID; + QString modName; + QString gameName; + QString nexusKey; + int modID; + int fileID; + int nexusExpires; + int nexusDownloadUser; + size_t fileSize; + QString fileName; + int fileCategory; + QDateTime fileTime; + QString repository; + + QVariantMap userData; + + QString author; + QString uploader; + QString uploaderUrl; +}; +} // namespace MOBase + +#endif // MODREPOSITORYFILEINFO_H diff --git a/libs/uibase/include/uibase/nxmurl.h b/libs/uibase/include/uibase/nxmurl.h new file mode 100644 index 0000000..bd06d3d --- /dev/null +++ b/libs/uibase/include/uibase/nxmurl.h @@ -0,0 +1,87 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef NXMURL_H +#define NXMURL_H + +#include "dllimport.h" +#include +#include +#include + +/** + * @brief represents a nxm:// url + * @todo the game name encoded into the url is not interpreted + **/ +class QDLLEXPORT NXMUrl : public QObject +{ + Q_OBJECT + +public: + /** + * @brief constructor + * + * @param url url following the nxm-protocol + **/ + NXMUrl(const QString& url); + + /** + * @return name of the game + */ + QString game() const { return m_Game; } + + /** + * @return the NMX request key + */ + QString key() const { return m_Key; } + + /** + * @brief retrieve the mod id encoded into the url + * + * @return mod id + **/ + int modId() const { return m_ModId; } + + /** + * @brief retrieve the file id encoded into the url + * + * @return file id + **/ + int fileId() const { return m_FileId; } + + /** + * @return the expires timestamp + */ + int expires() const { return m_Expires; } + + /** + * @return the parsed user ID + */ + int userId() const { return m_UserId; } + +private: + QString m_Game; + QString m_Key; + int m_ModId; + int m_FileId; + int m_Expires; + int m_UserId; +}; + +#endif // NXMURL_H diff --git a/libs/uibase/include/uibase/pluginrequirements.h b/libs/uibase/include/uibase/pluginrequirements.h new file mode 100644 index 0000000..ff3668a --- /dev/null +++ b/libs/uibase/include/uibase/pluginrequirements.h @@ -0,0 +1,193 @@ +#ifndef PLUGINREQUIREMENTS_H +#define PLUGINREQUIREMENTS_H + +#include +#include +#include + +#include +#include + +#include "dllimport.h" + +namespace MOBase +{ + +class IOrganizer; +class IPluginDiagnose; + +/** + * @brief The interface for plugin requirements. + */ +class IPluginRequirement +{ +public: + class Problem + { + public: + /** + * @return a short description for the problem. + */ + QString shortDescription() const { return m_ShortDescription; } + + /** + * @return a long description for the problem. + */ + QString longDescription() const { return m_LongDescription; } + + /** + * + */ + Problem(QString shortDescription, QString longDescription = "") + : m_ShortDescription(shortDescription), + m_LongDescription(longDescription.isEmpty() ? shortDescription + : longDescription) + {} + + private: + QString m_ShortDescription, m_LongDescription; + }; + +public: + /** + * @brief Check if the requirements is met. + * + * @param organizer The organizer proxy. + * + * @return a problem if the requirement is not met, otherwise an empty optional. + */ + virtual std::optional check(IOrganizer* organizer) const = 0; + + virtual ~IPluginRequirement() {} +}; + +/** + * @brief Plugin dependency - The requirement is met if one of the + * given plugin is active. + */ +class QDLLEXPORT PluginDependencyRequirement : public IPluginRequirement +{ + + friend class PluginRequirementFactory; + +public: + PluginDependencyRequirement(QStringList const& pluginNames); + + virtual std::optional check(IOrganizer* organizer) const; + + /** + * @return the list of plugin names in this dependency requirement. + */ + QStringList pluginNames() const { return m_PluginNames; } + +protected: + QString message() const; + QStringList m_PluginNames; +}; + +/** + * @brief Game dependency - The requirement is met if the active game + * is one of the specified game. + */ +class QDLLEXPORT GameDependencyRequirement : public IPluginRequirement +{ + + friend class PluginRequirementFactory; + +public: + GameDependencyRequirement(QStringList const& gameNames); + + virtual std::optional check(IOrganizer* organizer) const; + + /** + * @return the list of game names in this dependency requirement. + */ + QStringList gameNames() const { return m_GameNames; } + +protected: + QString message() const; + QStringList m_GameNames; +}; + +/** + * @brief Diagnose dependency - This wrap a IPluginDiagnose into a plugin + * requirements. + * + * If the wrapped diagnose plugin reports a problem, the requirement fails + * and the associated message is the one from the diagnose plugin (or the + * list of messages if multiple problems were reported). + */ +class DiagnoseRequirement : public IPluginRequirement +{ + + friend class PluginRequirementFactory; + +public: + DiagnoseRequirement(const IPluginDiagnose* diagnose); + + virtual std::optional check(IOrganizer* organizer) const; + +private: + const IPluginDiagnose* m_Diagnose; +}; + +/** + * Factory for plugin requirements. + */ +class QDLLEXPORT PluginRequirementFactory +{ +public: + /** + * @brief Create a new plugin dependency. The requirement is met if one of the + * given plugin is enabled. + * + * If you want all plugins to be active, simply create multiple requirements. + * + * @param pluginNames Name of the plugin required. + */ + static std::shared_ptr + pluginDependency(QStringList const& pluginNames); + static std::shared_ptr + pluginDependency(QString const& pluginName) + { + return pluginDependency(QStringList{pluginName}); + } + + /** + * @brief Create a new plugin dependency. The requirement is met if the current + * game plugin matches one of the given name. + * + * @param gameName Name of the game required. + * + * @note This differ from makePluginDependency only for the message. + */ + static std::shared_ptr + gameDependency(QStringList const& gameNames); + static std::shared_ptr + gameDependency(QString const& gameName) + { + return gameDependency(QStringList{gameName}); + } + + /** + * @brief Create a requirement from the given diagnose plugin. + * + * @param diagnose The diagnose plugin. + */ + static std::shared_ptr + diagnose(const IPluginDiagnose* diagnose); + + /** + * @brief Create a generic requirement with the given checker and message. + * + * @param checker The function to use to check if the requirement is met (should + * return true if the requirement is met). + * @param description The description to show user if the requirement is not met. + */ + static std::shared_ptr + basic(std::function const& checker, QString const description); +}; + +} // namespace MOBase + +#endif diff --git a/libs/uibase/include/uibase/pluginsetting.h b/libs/uibase/include/uibase/pluginsetting.h new file mode 100644 index 0000000..6844f4d --- /dev/null +++ b/libs/uibase/include/uibase/pluginsetting.h @@ -0,0 +1,50 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 PLUGINSETTING_H +#define PLUGINSETTING_H + +#include +#include +#include + +namespace MOBase +{ + +/** + * @brief struct to hold the user-configurable parameters a plugin accepts. The purpose + * of this struct is only to inform the application what settings to offer to the user, + * it does not hold the actual value + */ +struct PluginSetting +{ + PluginSetting(const QString& key, const QString& description, + const QVariant& defaultValue) + : key(key), description(description), defaultValue(defaultValue) + {} + + QString key; + QString description; + QVariant defaultValue; +}; + +} // namespace MOBase + +#endif // PLUGINSETTING_H diff --git a/libs/uibase/include/uibase/questionboxmemory.h b/libs/uibase/include/uibase/questionboxmemory.h new file mode 100644 index 0000000..c5224ea --- /dev/null +++ b/libs/uibase/include/uibase/questionboxmemory.h @@ -0,0 +1,112 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 QUESTIONBOXMEMORY_H +#define QUESTIONBOXMEMORY_H + +#include "dllimport.h" + +#include +#include +#include +#include +#include + +class QAbstractButton; +class QMutex; +class QSettings; +class QWidget; + +namespace Ui +{ +class QuestionBoxMemory; +} + +namespace MOBase +{ + +class QDLLEXPORT QuestionBoxMemory : public QDialog +{ + Q_OBJECT + +public: + using Button = QDialogButtonBox::StandardButton; + static const auto NoButton = QDialogButtonBox::NoButton; + + using GetButton = std::function; + using SetWindowButton = std::function; + using SetFileButton = std::function; + + ~QuestionBoxMemory(); + + // QuestionBoxMemory needs to access the settings, but they're only in + // the modorganizer project; the only way to avoid accessing the ini file + // directly is to use callbacks registered in Settings' constructor + // + static void setCallbacks(GetButton get, SetWindowButton setWindow, + SetFileButton setFile); + + static Button + query(QWidget* parent, const QString& windowName, const QString& title, + const QString& text, + QDialogButtonBox::StandardButtons buttons = QDialogButtonBox::Yes | + QDialogButtonBox::No, + QDialogButtonBox::StandardButton defaultButton = QDialogButtonBox::NoButton); + + static Button + query(QWidget* parent, const QString& windowName, const QString& fileName, + const QString& title, const QString& text, + QDialogButtonBox::StandardButtons buttons = QDialogButtonBox::Yes | + QDialogButtonBox::No, + QDialogButtonBox::StandardButton defaultButton = QDialogButtonBox::NoButton); + + static void setWindowMemory(const QString& windowName, Button b); + + static void setFileMemory(const QString& windowName, const QString& filename, + Button b); + + static Button getMemory(const QString& windowName, const QString& filename); + + static QString buttonToString(Button b); + +private slots: + void buttonClicked(QAbstractButton* button); + +private: + explicit QuestionBoxMemory(QWidget* parent, const QString& title, const QString& text, + const QString* filename, + const QDialogButtonBox::StandardButtons buttons, + QDialogButtonBox::StandardButton defaultButton); + +private: + static Button queryImpl( + QWidget* parent, const QString& windowName, const QString* fileName, + const QString& title, const QString& text, + QDialogButtonBox::StandardButtons buttons = QDialogButtonBox::Yes | + QDialogButtonBox::No, + QDialogButtonBox::StandardButton defaultButton = QDialogButtonBox::NoButton); + + std::unique_ptr ui; + QDialogButtonBox::StandardButton m_Button; +}; + +} // namespace MOBase + +#endif // QUESTIONBOXMEMORY_H diff --git a/libs/uibase/include/uibase/registry.h b/libs/uibase/include/uibase/registry.h new file mode 100644 index 0000000..00013df --- /dev/null +++ b/libs/uibase/include/uibase/registry.h @@ -0,0 +1,39 @@ +/* +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef REGISTRY_H +#define REGISTRY_H + +#include "dllimport.h" +#include + +namespace MOBase +{ + +// On Linux, writes INI values using QSettings instead of WritePrivateProfileString +QDLLEXPORT bool WriteRegistryValue(const QString& appName, const QString& keyName, + const QString& value, const QString& fileName); + +#ifdef _WIN32 +// Windows-specific overload using wide strings +QDLLEXPORT bool WriteRegistryValue(const wchar_t* appName, const wchar_t* keyName, + const wchar_t* value, const wchar_t* fileName); +#endif + +} // namespace MOBase + +#endif // REGISTRY_H diff --git a/libs/uibase/include/uibase/report.h b/libs/uibase/include/uibase/report.h new file mode 100644 index 0000000..f164681 --- /dev/null +++ b/libs/uibase/include/uibase/report.h @@ -0,0 +1,117 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 REPORT_H +#define REPORT_H + +#include "dllimport.h" +#include +#include +#include +#include +#include +#include + +namespace Ui +{ +class TaskDialog; +} + +namespace MOBase +{ + +class ExpanderWidget; + +// Convenience function displaying an error message box. This function uses +// WinAPI if no Qt Window is available yet or QMessageBox otherwise. +// +QDLLEXPORT void reportError(const QString& message); + +// shows a critical message box that's raised to the top of the zorder, useful +// for messages without a main window, which sometimes makes them pop up behind +// all other windows +// +// the dialog is not topmost, it's just raised once when shown +// +QDLLEXPORT void criticalOnTop(const QString& message); + +struct QDLLEXPORT TaskDialogButton +{ + QString text, description; + QMessageBox::StandardButton button; + + TaskDialogButton(QString text, QString description, + QMessageBox::StandardButton button); + TaskDialogButton(QString text, QMessageBox::StandardButton button); +}; + +class QDLLEXPORT TaskDialog +{ +public: + TaskDialog(QWidget* parent = nullptr, QString title = {}); + ~TaskDialog(); + + TaskDialog& title(const QString& s); + TaskDialog& main(const QString& s); + TaskDialog& content(const QString& s); + TaskDialog& details(const QString& s); + TaskDialog& icon(QMessageBox::Icon i); + TaskDialog& button(TaskDialogButton b); + TaskDialog& remember(const QString& action, const QString& file = {}); + + void addContent(QWidget* w); + void setWidth(int w); + + QMessageBox::StandardButton exec(); + +private: + std::unique_ptr m_dialog; + std::unique_ptr ui; + QString m_title, m_main, m_content, m_details; + QMessageBox::Icon m_icon; + std::vector m_buttons; + QMessageBox::StandardButton m_result; + std::unique_ptr m_expander; + int m_width; + + QString m_rememberAction, m_rememberFile; + QCheckBox* m_rememberCheck; + QComboBox* m_rememberCombo; + + QMessageBox::StandardButton checkMemory() const; + void rememberChoice(); + + void setDialog(); + void setWidgets(); + void setChoices(); + void setButtons(); + void setStandardButtons(); + void setCommandButtons(); + void setDetails(); + + QColor detailsColor() const; + QPixmap standardIcon(QMessageBox::Icon icon) const; + void setVisibleLines(QPlainTextEdit* w, int lines); + void setFontPercent(QWidget* w, double p); +}; + +} // namespace MOBase + +#endif // REPORT_H diff --git a/libs/uibase/include/uibase/safewritefile.h b/libs/uibase/include/uibase/safewritefile.h new file mode 100644 index 0000000..3ce9576 --- /dev/null +++ b/libs/uibase/include/uibase/safewritefile.h @@ -0,0 +1,72 @@ +/* +Copyright (C) 2014 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef SAFEWRITEFILE_H +#define SAFEWRITEFILE_H + +#include "dllimport.h" +#include "utility.h" +#include +#include +#include + +namespace MOBase +{ + +#ifndef _WIN32 +// Thin QFile wrapper that adds a no-op commit() for QSaveFile API compat. +class DirectWriteFile : public QFile +{ +public: + using QFile::QFile; + bool commit() + { + flush(); + return true; + } +}; +#endif + +/** + * @brief a wrapper for QSaveFile that handles errors when opening the file to reduce + * code duplication. On Linux, uses plain QFile because QSaveFile's temp-file + * strategy fails on many common filesystem configurations. + */ +class QDLLEXPORT SafeWriteFile +{ +public: + SafeWriteFile(const QString& fileName); + +#ifdef _WIN32 + QSaveFile* operator->(); +#else + DirectWriteFile* operator->(); +#endif + +private: +#ifdef _WIN32 + QSaveFile m_SaveFile; +#else + DirectWriteFile m_File; +#endif +}; + +} // namespace MOBase + +#endif // SAFEWRITEFILE_H diff --git a/libs/uibase/include/uibase/scopeguard.h b/libs/uibase/include/uibase/scopeguard.h new file mode 100644 index 0000000..11320f3 --- /dev/null +++ b/libs/uibase/include/uibase/scopeguard.h @@ -0,0 +1,296 @@ +#ifndef SCOPEGUARD_H +#define SCOPEGUARD_H + +/* + Scopeguard, by Andrei Alexandrescu and Petru Marginean, December 2000. + Modified by Joshua Lehrer, FactSet Research Systems, November 2005. +*/ + +template +class RefHolder +{ + T& ref_; + +public: + RefHolder(T& ref) : ref_(ref) {} + operator T&() const { return ref_; } + +private: + // Disable assignment - not implemented + RefHolder& operator=(const RefHolder&); +}; + +template +inline RefHolder ByRef(T& t) +{ + return RefHolder(t); +} + +class ScopeGuardImplBase +{ + ScopeGuardImplBase& operator=(const ScopeGuardImplBase&); + +protected: + ~ScopeGuardImplBase() {} + ScopeGuardImplBase(const ScopeGuardImplBase& other) throw() + : dismissed_(other.dismissed_) + { + other.Dismiss(); + } + template + static void SafeExecute(J& j) throw() + { + if (!j.dismissed_) + try { + j.Execute(); + } catch (...) { + } + } + + mutable bool dismissed_; + +public: + ScopeGuardImplBase() throw() : dismissed_(false) {} + void Dismiss() const throw() { dismissed_ = true; } +}; + +typedef const ScopeGuardImplBase& ScopeGuard; + +template +class ScopeGuardImpl0 : public ScopeGuardImplBase +{ +public: + static ScopeGuardImpl0 MakeGuard(F fun) { return ScopeGuardImpl0(fun); } + ~ScopeGuardImpl0() throw() { SafeExecute(*this); } + void Execute() { fun_(); } + +protected: + ScopeGuardImpl0(F fun) : fun_(fun) {} + F fun_; +}; + +template +inline ScopeGuardImpl0 MakeGuard(F fun) +{ + return ScopeGuardImpl0::MakeGuard(fun); +} + +template +class ScopeGuardImpl1 : public ScopeGuardImplBase +{ +public: + static ScopeGuardImpl1 MakeGuard(F fun, P1 p1) + { + return ScopeGuardImpl1(fun, p1); + } + ~ScopeGuardImpl1() throw() { SafeExecute(*this); } + void Execute() { fun_(p1_); } + +protected: + ScopeGuardImpl1(F fun, P1 p1) : fun_(fun), p1_(p1) {} + F fun_; + const P1 p1_; +}; + +template +inline ScopeGuardImpl1 MakeGuard(F fun, P1 p1) +{ + return ScopeGuardImpl1::MakeGuard(fun, p1); +} + +template +class ScopeGuardImpl2 : public ScopeGuardImplBase +{ +public: + static ScopeGuardImpl2 MakeGuard(F fun, P1 p1, P2 p2) + { + return ScopeGuardImpl2(fun, p1, p2); + } + ~ScopeGuardImpl2() throw() { SafeExecute(*this); } + void Execute() { fun_(p1_, p2_); } + +protected: + ScopeGuardImpl2(F fun, P1 p1, P2 p2) : fun_(fun), p1_(p1), p2_(p2) {} + F fun_; + const P1 p1_; + const P2 p2_; +}; + +template +inline ScopeGuardImpl2 MakeGuard(F fun, P1 p1, P2 p2) +{ + return ScopeGuardImpl2::MakeGuard(fun, p1, p2); +} + +template +class ScopeGuardImpl3 : public ScopeGuardImplBase +{ +public: + static ScopeGuardImpl3 MakeGuard(F fun, P1 p1, P2 p2, P3 p3) + { + return ScopeGuardImpl3(fun, p1, p2, p3); + } + ~ScopeGuardImpl3() throw() { SafeExecute(*this); } + void Execute() { fun_(p1_, p2_, p3_); } + +protected: + ScopeGuardImpl3(F fun, P1 p1, P2 p2, P3 p3) : fun_(fun), p1_(p1), p2_(p2), p3_(p3) {} + F fun_; + const P1 p1_; + const P2 p2_; + const P3 p3_; +}; + +template +inline ScopeGuardImpl3 MakeGuard(F fun, P1 p1, P2 p2, P3 p3) +{ + return ScopeGuardImpl3::MakeGuard(fun, p1, p2, p3); +} + +//************************************************************ + +template +class ObjScopeGuardImpl0 : public ScopeGuardImplBase +{ +public: + static ObjScopeGuardImpl0 MakeObjGuard(Obj& obj, MemFun memFun) + { + return ObjScopeGuardImpl0(obj, memFun); + } + ~ObjScopeGuardImpl0() throw() { SafeExecute(*this); } + void Execute() { (obj_.*memFun_)(); } + +protected: + ObjScopeGuardImpl0(Obj& obj, MemFun memFun) : obj_(obj), memFun_(memFun) {} + Obj& obj_; + MemFun memFun_; +}; + +template +inline ObjScopeGuardImpl0 MakeObjGuard(Obj& obj, MemFun memFun) +{ + return ObjScopeGuardImpl0::MakeObjGuard(obj, memFun); +} + +template +inline ObjScopeGuardImpl0 MakeGuard(Ret (Obj2::*memFun)(), + Obj1& obj) +{ + return ObjScopeGuardImpl0::MakeObjGuard(obj, memFun); +} + +template +inline ObjScopeGuardImpl0 MakeGuard(Ret (Obj2::*memFun)(), + Obj1* obj) +{ + return ObjScopeGuardImpl0::MakeObjGuard(*obj, memFun); +} + +template +class ObjScopeGuardImpl1 : public ScopeGuardImplBase +{ +public: + static ObjScopeGuardImpl1 MakeObjGuard(Obj& obj, MemFun memFun, + P1 p1) + { + return ObjScopeGuardImpl1(obj, memFun, p1); + } + ~ObjScopeGuardImpl1() throw() { SafeExecute(*this); } + void Execute() { (obj_.*memFun_)(p1_); } + +protected: + ObjScopeGuardImpl1(Obj& obj, MemFun memFun, P1 p1) + : obj_(obj), memFun_(memFun), p1_(p1) + {} + Obj& obj_; + MemFun memFun_; + const P1 p1_; +}; + +template +inline ObjScopeGuardImpl1 MakeObjGuard(Obj& obj, MemFun memFun, P1 p1) +{ + return ObjScopeGuardImpl1::MakeObjGuard(obj, memFun, p1); +} + +template +inline ObjScopeGuardImpl1 +MakeGuard(Ret (Obj2::*memFun)(P1a), Obj1& obj, P1b p1) +{ + return ObjScopeGuardImpl1::MakeObjGuard(obj, memFun, + p1); +} + +template +inline ObjScopeGuardImpl1 +MakeGuard(Ret (Obj2::*memFun)(P1a), Obj1* obj, P1b p1) +{ + return ObjScopeGuardImpl1::MakeObjGuard(*obj, memFun, + p1); +} + +template +class ObjScopeGuardImpl2 : public ScopeGuardImplBase +{ +public: + static ObjScopeGuardImpl2 MakeObjGuard(Obj& obj, MemFun memFun, + P1 p1, P2 p2) + { + return ObjScopeGuardImpl2(obj, memFun, p1, p2); + } + ~ObjScopeGuardImpl2() throw() { SafeExecute(*this); } + void Execute() { (obj_.*memFun_)(p1_, p2_); } + +protected: + ObjScopeGuardImpl2(Obj& obj, MemFun memFun, P1 p1, P2 p2) + : obj_(obj), memFun_(memFun), p1_(p1), p2_(p2) + {} + Obj& obj_; + MemFun memFun_; + const P1 p1_; + const P2 p2_; +}; + +template +inline ObjScopeGuardImpl2 MakeObjGuard(Obj& obj, MemFun memFun, + P1 p1, P2 p2) +{ + return ObjScopeGuardImpl2::MakeObjGuard(obj, memFun, p1, p2); +} + +template +inline ObjScopeGuardImpl2 +MakeGuard(Ret (Obj2::*memFun)(P1a, P2a), Obj1& obj, P1b p1, P2b p2) +{ + return ObjScopeGuardImpl2::MakeObjGuard( + obj, memFun, p1, p2); +} + +template +inline ObjScopeGuardImpl2 +MakeGuard(Ret (Obj2::*memFun)(P1a, P2a), Obj1* obj, P1b p1, P2b p2) +{ + return ObjScopeGuardImpl2::MakeObjGuard( + *obj, memFun, p1, p2); +} + +#define CONCATENATE_DIRECT(s1, s2) s1##s2 +#define CONCATENATE(s1, s2) CONCATENATE_DIRECT(s1, s2) +#define ANONYMOUS_VARIABLE(str) CONCATENATE(str, __LINE__) + +// #define ON_BLOCK_EXIT ScopeGuard ANONYMOUS_VARIABLE(scopeGuard) = MakeGuard +#define ON_BLOCK_EXIT(arg) \ + ScopeGuard ANONYMOUS_VARIABLE(scopeGuard) = MakeGuard(arg); \ + UNUSED_VAR(ANONYMOUS_VARIABLE(scopeGuard)) +// #define ON_BLOCK_EXIT_OBJ ScopeGuard ANONYMOUS_VARIABLE(scopeGuard) = MakeObjGuard + +#define UNUSED_VAR(VAR) (void)VAR + +namespace MOBase +{ + +} // namespace MOBase + +#endif // SCOPEGUARD_H diff --git a/libs/uibase/include/uibase/sortabletreewidget.h b/libs/uibase/include/uibase/sortabletreewidget.h new file mode 100644 index 0000000..4dc7881 --- /dev/null +++ b/libs/uibase/include/uibase/sortabletreewidget.h @@ -0,0 +1,64 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2013 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 SORTABLETREEWIDGET_H +#define SORTABLETREEWIDGET_H + +#include "dllimport.h" +#include + +namespace MOBase +{ + +class QDLLEXPORT SortableTreeWidget : public QTreeWidget +{ + Q_OBJECT + +public: + SortableTreeWidget(QWidget* parent = nullptr); + + /** + * @brief sets whether sorting is allowed only within the branch of a tree + * @param localOnly if true, objects can only be moved within a branch. if false + * (default) they can be moved to a different branch + */ + void setLocalMoveOnly(bool localOnly); +signals: + /** + * @brief signaled when items got moved. minimal atm + */ + void itemsMoved(); + +protected: + virtual void dropEvent(QDropEvent* event); + virtual bool dropMimeData(QTreeWidgetItem* parent, int index, const QMimeData* data, + Qt::DropAction action); + virtual Qt::DropActions supportedDropActions() const; + +private: + bool moveSelection(QTreeWidgetItem* parent, int index); + +private: + bool m_LocalMoveOnly; +}; + +} // namespace MOBase + +#endif // SORTABLETREEWIDGET_H diff --git a/libs/uibase/include/uibase/steamutility.h b/libs/uibase/include/uibase/steamutility.h new file mode 100644 index 0000000..4789ad3 --- /dev/null +++ b/libs/uibase/include/uibase/steamutility.h @@ -0,0 +1,47 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2019 MO2 Contributors. 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 STEAMUTILITY_H +#define STEAMUTILITY_H + +#include "dllimport.h" +#include +#include + +namespace MOBase +{ + +/** + * @brief Gets the installation path to Steam according to the registy + **/ +QDLLEXPORT QString findSteam(); + +/** + * @brief Gets the installation path to a Steam game + * + * @param appName The Steam application name, i.e., the expected steamapps folder name + * @param validFile If the given file exists in the found installation path, the game is + consider to be valid. May be blank. + **/ +QDLLEXPORT QString findSteamGame(const QString& appName, const QString& validFile); + +} // namespace MOBase + +#endif // STEAMUTILITY_H diff --git a/libs/uibase/include/uibase/strings.h b/libs/uibase/include/uibase/strings.h new file mode 100644 index 0000000..be388ab --- /dev/null +++ b/libs/uibase/include/uibase/strings.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +#include "dllimport.h" + +namespace MOBase +{ +#ifdef __cplusplus +extern "C++" { +#endif + +QDLLEXPORT void ireplace_all(std::string& input, std::string_view search, + std::string_view replace) noexcept; + +QDLLEXPORT bool iequals(std::string_view lhs, std::string_view rhs); + +#ifdef __cplusplus +} +#endif +} // namespace MOBase diff --git a/libs/uibase/include/uibase/taskprogressmanager.h b/libs/uibase/include/uibase/taskprogressmanager.h new file mode 100644 index 0000000..8dba8d1 --- /dev/null +++ b/libs/uibase/include/uibase/taskprogressmanager.h @@ -0,0 +1,60 @@ +#ifndef TASKPROGRESSMANAGER_H +#define TASKPROGRESSMANAGER_H + +#include "dllimport.h" +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +using HWND = void*; +#endif + +namespace MOBase +{ + +class QDLLEXPORT TaskProgressManager : QObject +{ + + Q_OBJECT + +public: + static TaskProgressManager& instance(); + + void forgetMe(quint32 id); + void updateProgress(quint32 id, qint64 value, qint64 max); + + quint32 getId(); + +public slots: + bool tryCreateTaskbar(); + +private: + TaskProgressManager(); + void showProgress(); + +private: + std::map> m_Percentages; + QMutex m_Mutex; + quint32 m_NextId; + QTimer m_CreateTimer; + int m_CreateTries; + + HWND m_WinId; + +#ifdef _WIN32 + ITaskbarList3* m_Taskbar; +#else + void* m_Taskbar; +#endif +}; + +} // namespace MOBase + +#endif // TASKPROGRESSMANAGER_H diff --git a/libs/uibase/include/uibase/textviewer.h b/libs/uibase/include/uibase/textviewer.h new file mode 100644 index 0000000..4f5dfa2 --- /dev/null +++ b/libs/uibase/include/uibase/textviewer.h @@ -0,0 +1,97 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 TEXTVIEWER_H +#define TEXTVIEWER_H + +#include "dllimport.h" +#include +#include +#include +#include + +namespace Ui +{ +class TextViewer; +} + +namespace MOBase +{ + +class FindDialog; + +/** + * @brief rudimentary tabbed text editor + **/ +class QDLLEXPORT TextViewer : public QDialog +{ + Q_OBJECT + +public: + /** + * @brief constructor + * @param parent parent widget + **/ + explicit TextViewer(const QString& title, QWidget* parent = 0); + + ~TextViewer(); + + /** + * @brief set the description text to inform the user what he's editing + * + * @param description the description to set + **/ + void setDescription(const QString& description); + + /** + * @brief add a new tab with the specified file open + * + * @param fileName name of the file to open + * @param writable if true, the file can be modified + **/ + void addFile(const QString& fileName, bool writable); + +protected: + void closeEvent(QCloseEvent* event); + bool eventFilter(QObject* obj, QEvent* event); + +private slots: + + void saveFile(); + void modified(); + void patternChanged(QString newPattern); + void findNext(); + void showWhitespaceChanged(int state); + +private: + void saveFile(const QTextEdit* editor); + void find(); + +private: + Ui::TextViewer* ui; + QTabWidget* m_EditorTabs; + std::set m_Modified; + FindDialog* m_FindDialog; + QString m_FindPattern; +}; + +} // namespace MOBase + +#endif // TEXTVIEWER_H diff --git a/libs/uibase/include/uibase/tutorabledialog.h b/libs/uibase/include/uibase/tutorabledialog.h new file mode 100644 index 0000000..aaaa093 --- /dev/null +++ b/libs/uibase/include/uibase/tutorabledialog.h @@ -0,0 +1,65 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 TUTORABLEDIALOG_H +#define TUTORABLEDIALOG_H + +#include "dllimport.h" +#include "tutorialcontrol.h" +#include +#include +#include + +namespace MOBase +{ + +/** + * @brief A dialog for which a tutorial can be displayed. Dialogs should derive from + * this instead of QDialog and delegate all showEvent- and resizeEvent-calls to + * TutorableDialog for tutorials to work + */ +class QDLLEXPORT TutorableDialog : public QDialog +{ + Q_OBJECT + +public: + /** + * @brief constructor + * @param name a unique name for this dialog type. This is used to refer to the dialog + * from tutorials + * @param parent the parent widget of the dialog + */ + explicit TutorableDialog(const QString& name, QWidget* parent = 0); + +signals: + +public slots: + +protected: + void showEvent(QShowEvent* event); + void resizeEvent(QResizeEvent* event); + +private: + TutorialControl m_Tutorial; +}; + +} // namespace MOBase + +#endif // TUTORABLEDIALOG_H diff --git a/libs/uibase/include/uibase/tutorialcontrol.h b/libs/uibase/include/uibase/tutorialcontrol.h new file mode 100644 index 0000000..4dc85f6 --- /dev/null +++ b/libs/uibase/include/uibase/tutorialcontrol.h @@ -0,0 +1,83 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 TUTORIALCONTROL_H +#define TUTORIALCONTROL_H + +#include "dllimport.h" +#include +#include +#include +#include + +namespace MOBase +{ + +class TutorialManager; + +class QDLLEXPORT TutorialControl : public QObject +{ + Q_OBJECT +public: + TutorialControl(const TutorialControl& reference); + TutorialControl(QWidget* targetControl, const QString& name); + + ~TutorialControl(); + + void registerControl(); + void resize(const QSize& size); + + void expose(const QString& widgetName, QObject* widget); + + void startTutorial(const QString& tutorial); + + Q_INVOKABLE void finish(); + Q_INVOKABLE QRect getRect(const QString& widgetName); + Q_INVOKABLE QRect getActionRect(const QString& widgetName); + Q_INVOKABLE QRect getMenuRect(const QString& widgetName); + Q_INVOKABLE QWidget* getChild(const QString& name); + Q_INVOKABLE bool waitForButton(const QString& buttonName); + Q_INVOKABLE bool waitForAction(const QString& actionName); + Q_INVOKABLE bool waitForTabOpen(const QString& tabControlName, const QString& tab); + Q_INVOKABLE const QString getTabName(const QString& tabControlName); + Q_INVOKABLE void lockUI(bool locked); + Q_INVOKABLE void simulateClick(int x, int y); + +private slots: + + void tabChangedProxy(int selected); + void nextTutorialStepProxy(); + +private: + QWidget* m_TargetControl; + QString m_Name; + QQuickWidget* m_TutorialView; + + TutorialManager& m_Manager; + + std::vector> m_ExposedObjects; + QList m_Connections; + int m_ExpectedTab; + QWidget* m_CurrentClickControl; +}; + +} // namespace MOBase + +#endif // TUTORIALCONTROL_H diff --git a/libs/uibase/include/uibase/tutorialmanager.h b/libs/uibase/include/uibase/tutorialmanager.h new file mode 100644 index 0000000..8120164 --- /dev/null +++ b/libs/uibase/include/uibase/tutorialmanager.h @@ -0,0 +1,109 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 TUTORIALMANAGER_H +#define TUTORIALMANAGER_H + +#include "dllimport.h" +#include +#include + +namespace MOBase +{ + +class TutorialControl; + +class QDLLEXPORT TutorialManager : public QObject +{ + + Q_OBJECT + +public: + /** + * @brief set up the tutorial manager + * @param path to where the tutorials are stored + */ + static void init(const QString& tutorialPath, QObject* organizerCore); + + static TutorialManager& instance(); + + QObject* organizerCore() { return m_OrganizerCore; } + + /** + * @brief registers a control that can be used to display tutorial messages in one + * window. This this called when the window becomes visible + * @param windowName name of the window. This is used to reference the window from + * qml/js files, as well as for the unregister-call + * @param control the control to register + */ + void registerControl(const QString& windowName, TutorialControl* control); + + /** + * @brief unregister a tutorial control. This is called when the window to which the + * control belongs gets closed + * @param windowName name of the control to hide + */ + void unregisterControl(const QString& windowName); + + /** + * @brief tests if the specified tutorial exists + * @param tutorialName name of the tutorial to test + * @return true if there is a tutorial with the specified name + */ + bool hasTutorial(const QString& tutorialName); + + /** + * @brief start a tutorial for the specified window + * @param windowName window for which to start the tutorial + * @param tutorialName name of the tutorial script to run + */ + Q_INVOKABLE void activateTutorial(const QString& windowName, + const QString& tutorialName); + + /** + * @brief mark a window tutorial as completed. It will not show up again + * @param windowName name of the window for which the tutorial completed + */ + Q_INVOKABLE void finishWindowTutorial(const QString& windowName); + + Q_INVOKABLE QWidget* findControl(const QString& controlName); +signals: + + void windowTutorialFinished(const QString& windowName); + + void tabChanged(int index); + +private: + TutorialManager(const QString& tutorialPath, QObject* organizerCore); + +private: + static TutorialManager* s_Instance; + + // QScriptEngine m_ScriptEngine; + QString m_TutorialPath; + QObject* m_OrganizerCore; + + std::map m_Controls; + std::map m_PendingTutorials; +}; + +} // namespace MOBase + +#endif // TUTORIALMANAGER_H diff --git a/libs/uibase/include/uibase/utility.h b/libs/uibase/include/uibase/utility.h new file mode 100644 index 0000000..c08133b --- /dev/null +++ b/libs/uibase/include/uibase/utility.h @@ -0,0 +1,443 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 MO_UIBASE_UTILITY_INCLUDED +#define MO_UIBASE_UTILITY_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +// POSIX compatibility types +#include "windows_compat.h" +#endif + +#include "dllimport.h" +#include "exceptions.h" + +namespace MOBase +{ + +/** + * @brief remove the specified directory including all sub-directories + * + * @param dirName name of the directory to delete + * @return true on success. in case of an error, "removeDir" itself displays an error + *message + **/ +QDLLEXPORT bool removeDir(const QString& dirName); + +/** + * @brief copy a directory recursively + * @param sourceName name of the directory to copy + * @param destinationName name of the target directory + * @param merge if true, the destination directory is allowed to exist, files will then + * be added to that directory. If false, the call will fail in that case + * @return true if files were copied. This doesn't necessary mean ALL files were copied + * @note symbolic links are not followed to prevent endless recursion + */ +QDLLEXPORT bool copyDir(const QString& sourceName, const QString& destinationName, + bool merge); + +/** + * @brief move a file, creating subdirectories as needed + * @param source source file name + * @param destination destination file name + * @return true if the file was successfully copied + */ +QDLLEXPORT bool moveFileRecursive(const QString& source, const QString& baseDir, + const QString& destination); + +/** + * @brief copy a file, creating subdirectories as needed + * @param source source file name + * @param destination destination file name + * @return true if the file was successfully copied + */ +QDLLEXPORT bool copyFileRecursive(const QString& source, const QString& baseDir, + const QString& destination); + +/** + * @brief copy one or multiple files using a shell operation (this will ask the user for + *confirmation on overwrite or elevation requirement) + * @param sourceNames names of files to be copied. This can include wildcards + * @param destinationNames names of the files in the destination location or the + *destination directory to copy to. There has to be one destination name for each source + *name or a single directory + * @param dialog a dialog to be the parent of possible confirmation dialogs + * @return true on success, false on error + **/ +QDLLEXPORT bool shellCopy(const QStringList& sourceNames, + const QStringList& destinationNames, + QWidget* dialog = nullptr); + +QDLLEXPORT bool shellCopy(const QString& sourceNames, const QString& destinationNames, + bool yesToAll = false, QWidget* dialog = nullptr); + +QDLLEXPORT bool shellMove(const QStringList& sourceNames, + const QStringList& destinationNames, + QWidget* dialog = nullptr); + +QDLLEXPORT bool shellMove(const QString& sourceNames, const QString& destinationNames, + bool yesToAll = false, QWidget* dialog = nullptr); + +QDLLEXPORT bool shellRename(const QString& oldName, const QString& newName, + bool yesToAll = false, QWidget* dialog = nullptr); + +QDLLEXPORT bool shellDelete(const QStringList& fileNames, bool recycle = false, + QWidget* dialog = nullptr); + +QDLLEXPORT bool shellDeleteQuiet(const QString& fileName, QWidget* dialog = nullptr); + +namespace shell +{ + namespace details + { + // used by HandlePtr on Windows, stub on Linux + struct HandleCloser + { + using pointer = HANDLE; + + void operator()(HANDLE h) + { +#ifdef _WIN32 + if (h != INVALID_HANDLE_VALUE) { + ::CloseHandle(h); + } +#else + (void)h; +#endif + } + }; + + using HandlePtr = std::unique_ptr; + } // namespace details + + // returned by the various shell functions + class QDLLEXPORT Result + { + public: + Result(bool success, DWORD error, QString message, HANDLE process); + + // non-copyable + Result(const Result&) = delete; + Result& operator=(const Result&) = delete; + Result(Result&&) = default; + Result& operator=(Result&&) = default; + + static Result makeFailure(DWORD error, QString message = {}); + static Result makeSuccess(HANDLE process = INVALID_HANDLE_VALUE); + + bool success() const; + explicit operator bool() const; + + DWORD error(); + + const QString& message() const; + + HANDLE processHandle() const; + + HANDLE stealProcessHandle(); + + QString toString() const; + + private: + bool m_success; + DWORD m_error; + QString m_message; + details::HandlePtr m_process; + }; + + QDLLEXPORT QString formatError(int i); + + QDLLEXPORT Result Explore(const QFileInfo& info); + QDLLEXPORT Result Explore(const QString& path); + QDLLEXPORT Result Explore(const QDir& dir); + + QDLLEXPORT Result Open(const QString& path); + QDLLEXPORT Result Open(const QUrl& url); + + QDLLEXPORT Result Execute(const QString& program, const QString& params = {}); + + QDLLEXPORT Result Delete(const QFileInfo& path); + + QDLLEXPORT Result Rename(const QFileInfo& src, const QFileInfo& dest); + QDLLEXPORT Result Rename(const QFileInfo& src, const QFileInfo& dest, + bool copyAllowed); + + QDLLEXPORT Result CreateDirectories(const QDir& dir); + QDLLEXPORT Result DeleteDirectoryRecursive(const QDir& dir); + + QDLLEXPORT void SetUrlHandler(const QString& cmd); +} // namespace shell + +template +QString VectorJoin(const std::vector& value, const QString& separator, + size_t maximum = UINT_MAX) +{ + QString result; + if (value.size() != 0) { + QTextStream stream(&result); + stream << value[0]; + for (unsigned int i = 1; i < (std::min)(value.size(), maximum); ++i) { + stream << separator << value[i]; + } + if (maximum < value.size()) { + stream << separator << "..."; + } + } + return result; +} + +template +QString SetJoin(const std::set& value, const QString& separator, + size_t maximum = UINT_MAX) +{ + QString result; + typename std::set::const_iterator iter = value.begin(); + if (iter != value.end()) { + QTextStream stream(&result); + stream << *iter; + ++iter; + unsigned int pos = 1; + for (; iter != value.end() && pos < maximum; ++iter) { + stream << separator << *iter; + } + if (maximum < value.size()) { + stream << separator << "..."; + } + } + return result; +} + +template +QList ConvertList(const QVariantList& variants) +{ + QList result; + for (const QVariant& var : variants) { + if (!var.canConvert()) { + throw Exception("invalid variant type"); + } + result.append(var.value()); + } +} + +QDLLEXPORT std::wstring ToWString(const QString& source); +QDLLEXPORT std::string ToString(const QString& source, bool utf8 = true); +QDLLEXPORT QString ToQString(const std::string& source); +QDLLEXPORT QString ToQString(const std::wstring& source); + +#ifdef _WIN32 +QDLLEXPORT QString ToString(const SYSTEMTIME& time); +#endif + +QDLLEXPORT int naturalCompare(const QString& a, const QString& b, + Qt::CaseSensitivity cs = Qt::CaseInsensitive); + +class QDLLEXPORT NaturalSort +{ +public: + NaturalSort(Qt::CaseSensitivity cs = Qt::CaseInsensitive) : m_cs(cs) {} + + bool operator()(const QString& a, const QString& b) + { + return (naturalCompare(a, b, m_cs) < 0); + } + +private: + Qt::CaseSensitivity m_cs; +}; + +#ifdef _WIN32 +QDLLEXPORT QDir getKnownFolder(KNOWNFOLDERID id, const QString& what = {}); +QDLLEXPORT QString getOptionalKnownFolder(KNOWNFOLDERID id); +#endif + +QDLLEXPORT QString getDesktopDirectory(); +QDLLEXPORT QString getStartMenuDirectory(); + +QDLLEXPORT QString readFileText(const QString& fileName, QString* encoding = nullptr, + bool* hadBOM = nullptr); + +QDLLEXPORT QString decodeTextData(const QByteArray& fileData, + QString* encoding = nullptr, bool* hadBOM = nullptr); + +QDLLEXPORT void removeOldFiles(const QString& path, const QString& pattern, + int numToKeep, QDir::SortFlags sorting = QDir::Time); + +QDLLEXPORT QIcon iconForExecutable(const QString& filePath); + +QDLLEXPORT QString getFileVersion(QString const& filepath); +QDLLEXPORT QString getProductVersion(QString const& program); + +QDLLEXPORT bool isWindowsDrivePath(const QString& path); +QDLLEXPORT bool isWineZDrivePath(const QString& path); +QDLLEXPORT QString toWinePath(const QString& path); +QDLLEXPORT QString fromWinePath(const QString& path); +QDLLEXPORT QString normalizePathForHost(const QString& path); +QDLLEXPORT QString normalizePathForWine(const QString& path); + +QDLLEXPORT void deleteChildWidgets(QWidget* w); + +template +bool isOneOf(const T& val, const std::initializer_list& list) +{ + return std::find(list.begin(), list.end(), val) != list.end(); +} + +QDLLEXPORT std::wstring formatSystemMessage(DWORD id); +QDLLEXPORT std::wstring formatNtMessage(NTSTATUS s); + +inline std::wstring formatSystemMessage(HRESULT hr) +{ + return formatSystemMessage(static_cast(hr)); +} + +QDLLEXPORT QString windowsErrorString(DWORD errorCode); + +QDLLEXPORT QString localizedByteSize(unsigned long long bytes); +QDLLEXPORT QString localizedByteSpeed(unsigned long long bytesPerSecond); + +QDLLEXPORT QString localizedTimeRemaining(unsigned int msecs); + +template +class Guard +{ +public: + Guard() : m_call(false) {} + + Guard(F f) : m_f(f), m_call(true) {} + + Guard(Guard&& g) : m_f(std::move(g.m_f)) { g.m_call = false; } + + ~Guard() + { + if (m_call) + m_f(); + } + + Guard& operator=(Guard&& g) + { + m_f = std::move(g.m_f); + g.m_call = false; + return *this; + } + + void kill() { m_call = false; } + + Guard(const Guard&) = delete; + Guard& operator=(const Guard&) = delete; + +private: + F m_f; + bool m_call; +}; + +class QDLLEXPORT TimeThis +{ +public: + TimeThis(const QString& what = {}); + ~TimeThis(); + + TimeThis(const TimeThis&) = delete; + TimeThis& operator=(const TimeThis&) = delete; + + void start(const QString& what = {}); + void stop(); + +private: + using Clock = std::chrono::high_resolution_clock; + + QString m_what; + Clock::time_point m_start; + bool m_running; +}; + +template +bool forEachLineInFile(const QString& filePath, F&& f) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + return false; + } + + QByteArray data = file.readAll(); + file.close(); + + const char* lineStart = data.constData(); + const char* p = lineStart; + const char* end = data.constData() + data.size(); + + while (p < end) { + // skip all newline characters + while ((p < end) && (*p == '\n' || *p == '\r')) { + ++p; + } + + // line starts here + lineStart = p; + + // find end of line + while ((p < end) && *p != '\n' && *p != '\r') { + ++p; + } + + if (p != lineStart) { + // skip whitespace at beginning of line, don't go past end of line + while (std::isspace(*lineStart) && lineStart < p) { + ++lineStart; + } + + // skip comments + if (*lineStart != '#') { + // skip line if it only had whitespace + if (lineStart < p) { + // skip white at end of line + const char* lineEnd = p - 1; + while (std::isspace(*lineEnd) && lineEnd > lineStart) { + --lineEnd; + } + ++lineEnd; + + f(QString::fromUtf8(lineStart, lineEnd - lineStart)); + } + } + } + } + + return true; +} + +} // namespace MOBase + +#endif // MO_UIBASE_UTILITY_INCLUDED diff --git a/libs/uibase/include/uibase/versioninfo.h b/libs/uibase/include/uibase/versioninfo.h new file mode 100644 index 0000000..fd5d481 --- /dev/null +++ b/libs/uibase/include/uibase/versioninfo.h @@ -0,0 +1,185 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 MODVERSION_H +#define MODVERSION_H + +#include "dllimport.h" +#include +#include + +class QVersionNumber; + +namespace MOBase +{ + +/** + * @brief represents the version of a mod or plugin + * + * this will try to parse machine-readable information from a string. + **/ +class QDLLEXPORT VersionInfo +{ + + friend QDLLEXPORT bool operator<(const VersionInfo& LHS, const VersionInfo& RHS); + friend QDLLEXPORT bool operator>(const VersionInfo& LHS, const VersionInfo& RHS); + friend QDLLEXPORT bool operator<=(const VersionInfo& LHS, const VersionInfo& RHS); + friend QDLLEXPORT bool operator>=(const VersionInfo& LHS, const VersionInfo& RHS); + friend QDLLEXPORT bool operator!=(const VersionInfo& LHS, const VersionInfo& RHS); + friend QDLLEXPORT bool operator==(const VersionInfo& LHS, const VersionInfo& RHS); + +public: + enum ReleaseType + { + RELEASE_PREALPHA, + RELEASE_ALPHA, + RELEASE_BETA, + RELEASE_CANDIDATE, + RELEASE_FINAL + }; + + enum VersionScheme + { + SCHEME_DISCOVER, // use regular scheme unless the string contains a hint that it's + // one of the others + SCHEME_REGULAR, + SCHEME_DECIMALMARK, // for schemes that treat the version as a decimal number with + // the dot as the decimal mark + SCHEME_NUMBERSANDLETTERS, // for schemes that mix numbers and letters + // (1.0.1a, 1.0.1c, ...). otherwise this is the regular + // scheme + SCHEME_DATE, // contains a release date instead of a version number + SCHEME_LITERAL // use the version string as is, unmodified + }; + +public: + /** + * @brief default constructor + * constructs an invalid version + **/ + VersionInfo(); + + /** + * @brief constructor + * @param major major version + * @param minor minor version + * @param subminor subminor version + * @param releaseType release type + */ + VersionInfo(int major, int minor, int subminor, + ReleaseType releaseType = RELEASE_FINAL); + + /** + * @brief constructor + * @param major major version + * @param minor minor version + * @param subminor subminor version + * @param subsubminor subsubminor version + * @param releaseType release type + */ + VersionInfo(int major, int minor, int subminor, int subsubminor, + ReleaseType releaseType = RELEASE_FINAL); + + /** + * @brief constructor + * @param versionString the string to construct from + **/ + VersionInfo(const QString& versionString, VersionScheme scheme = SCHEME_DISCOVER); + + /** + * @brief constructor + * @param versionString the string to construct from + * @param manualInput if true the versionString is treated as input from a user + **/ + VersionInfo(const QString& versionString, VersionScheme scheme, bool manualInput); + + /** + * @brief resets this structure to an invalid version + */ + void clear(); + + /** + * @brief parse the version from the specified string + * + * @param versionString the string to parse + **/ + void parse(const QString& versionString, VersionScheme scheme = SCHEME_DISCOVER, + bool manualInput = false); + + /** + * @return a canonicalized version string + * @note due to support for different versioning schemes this somewhat lost it's + *original intention. This is now supposed to return a version string that can be + *parsed to re-create this VersionInfo without information loss. + **/ + QString canonicalString() const; + + /** + * @return a version string for display to the user. This may loose information as it + * doesn't contain information about the versioning scheme + * + * @param forcedVersionSegments the number of version segments to display even if the + * version is 0. 1 is major, 2 is major and minor, etc. The only implemented ranges + * are (-inf,2] for major/minor, [3] for major/minor/subminor, and [4,inf) for + * major/minor/subminor/subsubminor. This only versions with a regular scheme. + */ + QString displayString(int forcedVersionSegments = 2) const; + + /** + * @return true if this version is valid, false if it wasn't initialised or + * the version string was not parsable + */ + bool isValid() const { return m_Valid; } + + /** + * @return the versioning scheme in effect + */ + VersionScheme scheme() const { return m_Scheme; } + + // returns this version number as a QVersionNumber + // + QVersionNumber asQVersionNumber() const; + +private: + /** + * @brief determine the release type + * @param versionString the version string to parse + * @return the version string with the release type removed + **/ + QString parseReleaseType(QString versionString); + +private: + VersionScheme m_Scheme; + + bool m_Valid; + ReleaseType m_ReleaseType; + int m_Major; + int m_Minor; + int m_SubMinor; + int m_SubSubMinor; + + int m_DecimalPositions; + + QString m_Rest; +}; + +} // namespace MOBase + +#endif // MODVERSION_H diff --git a/libs/uibase/include/uibase/versioning.h b/libs/uibase/include/uibase/versioning.h new file mode 100644 index 0000000..c4f9541 --- /dev/null +++ b/libs/uibase/include/uibase/versioning.h @@ -0,0 +1,181 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "dllimport.h" +#include "exceptions.h" + +namespace MOBase +{ + +class InvalidVersionException : public Exception +{ +public: + using Exception::Exception; +}; + +// class representing a Version object +// +// valid versions are an "extension" of SemVer (see https://semver.org/) with the +// following tweaks: +// - version can have a sub-patch, i.e., x.y.z.p, which are normally not allowed by +// SemVer +// - non-integer pre-release identifiers are limited to dev, alpha (a), beta (b) and rc, +// and dev is lower than alpha (according to SemVer, the pre-release should be +// ordered alphabetically) +// - the '-' between version and pre-release can be made optional, and also the '.' +// between pre-releases segment +// +// the extension from SemVer are only meant to be used by MO2 and USVFS versioning, +// plugins and extensions should follow SemVer standard (and not use dev), this is +// mainly +// - for back-compatibility purposes, because USVFS versioning contains sub-patches and +// there are old MO2 releases with sub-patch +// - because MO2 is not going to become MO3, so having an extra level make sense +// +// unlike VersionInfo, this class is immutable and only hold valid versions +// +class QDLLEXPORT Version +{ +public: + enum class ParseMode + { + // official semver parsing with pre-release limited to dev, alpha/a, beta/b and rc + // + SemVer, + + // MO2 parsing, e.g., 2.5.1rc1 - this either parse a string with no pre-release + // information (e.g. 2.5.1) or with a single pre-release + a version (e.g., 2.5.1a1 + // or 2.5.2rc1) + // + // this mode can parse sub-patch (SemVer mode cannot) + // + MO2 + }; + + enum class FormatMode + { + // show subpatch even if subpatch is 0 + // + ForceSubPatch = 0b0001, + + // do not add separators between version and pre-release (-) or between pre-release + // segments (.) + // + NoSeparator = 0b0010, + + // uses short form for alpha and beta (a/b instead of alpha/beta) + // + ShortAlphaBeta = 0b0100, + + // do not add metadata even if present + // + NoMetadata = 0b1000 + }; + Q_DECLARE_FLAGS(FormatModes, FormatMode); + + // condensed format, no separator, short alpha/beta and no metadata + // + static constexpr auto FormatCondensed = FormatModes{ + FormatMode::NoSeparator, FormatMode::ShortAlphaBeta, FormatMode::NoMetadata}; + + enum class ReleaseType + { + Development, // -dev + Alpha, // -alpha, -a + Beta, // -beta, -b + ReleaseCandidate, // -rc + }; + using enum ReleaseType; + +public: // parsing + // parse version from the given string, throw InvalidVersionException if the string + // cannot be parsed + // + static Version parse(QString const& value, ParseMode mode = ParseMode::SemVer); + +public: // constructors + Version(int major, int minor, int patch, QString metadata = {}); + Version(int major, int minor, int patch, int subpatch, QString metadata = {}); + + Version(int major, int minor, int patch, ReleaseType type, QString metadata = {}); + Version(int major, int minor, int patch, int subpatch, ReleaseType type, + QString metadata = {}); + + Version(int major, int minor, int patch, ReleaseType type, int prerelease, + QString metadata = {}); + Version(int major, int minor, int patch, int subpatch, ReleaseType type, + int prerelease, QString metadata = {}); + + Version(int major, int minor, int patch, int subpatch, + std::vector> prereleases, + QString metadata = {}); + +public: // special member functions + Version(const Version&) = default; + Version(Version&&) = default; + + Version& operator=(const Version&) = default; + Version& operator=(Version&&) = default; + +public: + // check if this version corresponds to a pre-release version (dev, alpha, beta, etc.) + // + bool isPreRelease() const { return !m_PreReleases.empty(); } + + // retrieve major, minor, patch and sub-patch of this version + // + int major() const { return m_Major; } + int minor() const { return m_Minor; } + int patch() const { return m_Patch; } + int subpatch() const { return m_SubPatch; } + + // retrieve pre-releases information for this version + // + const auto& preReleases() const { return m_PreReleases; } + + // retrieve build metadata, if any, otherwise return an empty string + // + const auto& buildMetadata() const { return m_BuildMetadata; } + + // convert this version to a string + // + QString string(const FormatModes& modes = {}) const; + +private: + // major.minor.patch + int m_Major, m_Minor, m_Patch, m_SubPatch; + + // pre-release information + std::vector> m_PreReleases; + + // metadata + QString m_BuildMetadata; +}; + +QDLLEXPORT std::strong_ordering operator<=>(const Version& lhs, const Version& rhs); + +inline bool operator==(const Version& lhs, const Version& rhs) +{ + return (lhs <=> rhs) == 0; +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(Version::FormatModes); + +} // namespace MOBase + +template +struct std::formatter : std::formatter +{ + template + FmtContext::iterator format(const MOBase::Version& v, FmtContext& ctx) const + { + return std::formatter::format(v.string(), ctx); + } +}; diff --git a/libs/uibase/include/uibase/widgetutility.h b/libs/uibase/include/uibase/widgetutility.h new file mode 100644 index 0000000..10dc6f9 --- /dev/null +++ b/libs/uibase/include/uibase/widgetutility.h @@ -0,0 +1,22 @@ +#ifndef UIBASE_WIDGETUTILITY_INCLUDED +#define UIBASE_WIDGETUTILITY_INCLUDED + +#include "dllimport.h" +#include +#include + +namespace MOBase +{ + +/** + * Custom user-role that can be used in conjunction with `setCustomizableColumns`. If + * a column has a value for this role, and the value is false, the checkbox + * corresponding to the column will be disabled, otherwise the checkbox is enabled. + */ +constexpr auto EnabledColumnRole = Qt::UserRole + 1; + +QDLLEXPORT void setCustomizableColumns(QTreeView* view); + +} // namespace MOBase + +#endif // UIBASE_WIDGETUTILITY_INCLUDED diff --git a/libs/uibase/include/uibase/windows_compat.h b/libs/uibase/include/uibase/windows_compat.h new file mode 120000 index 0000000..bce219f --- /dev/null +++ b/libs/uibase/include/uibase/windows_compat.h @@ -0,0 +1 @@ +../../../../src/src/shared/windows_compat.h \ No newline at end of file diff --git a/libs/uibase/src/CMakeLists.txt b/libs/uibase/src/CMakeLists.txt new file mode 100644 index 0000000..fff3ac3 --- /dev/null +++ b/libs/uibase/src/CMakeLists.txt @@ -0,0 +1,201 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Qt6 CONFIG REQUIRED COMPONENTS Network Qml Quick QuickWidgets Widgets) +find_package(spdlog CONFIG REQUIRED) +qt_standard_project_setup() + +set(root_headers + ../include/uibase/delayedfilewriter.h + ../include/uibase/diagnosisreport.h + ../include/uibase/dllimport.h + ../include/uibase/errorcodes.h + ../include/uibase/eventfilter.h + ../include/uibase/exceptions.h + ../include/uibase/executableinfo.h + ../include/uibase/filemapping.h + ../include/uibase/filesystemutilities.h + ../include/uibase/guessedvalue.h + ../include/uibase/idownloadmanager.h + ../include/uibase/json.h + ../include/uibase/log.h + ../include/uibase/memoizedlock.h + ../include/uibase/moassert.h + ../include/uibase/modrepositoryfileinfo.h + ../include/uibase/nxmurl.h + ../include/uibase/pluginrequirements.h + ../include/uibase/pluginsetting.h + ../include/uibase/registry.h + ../include/uibase/report.h + ../include/uibase/safewritefile.h + ../include/uibase/scopeguard.h + ../include/uibase/steamutility.h + ../include/uibase/strings.h + ../include/uibase/utility.h + ../include/uibase/versioning.h + ../include/uibase/versioninfo.h +) +set(interface_headers + ../include/uibase/iexecutable.h + ../include/uibase/iexecutableslist.h + ../include/uibase/ifiletree.h + ../include/uibase/ifiletree_utils.h + ../include/uibase/iinstallationmanager.h + ../include/uibase/iinstance.h + ../include/uibase/iinstancemanager.h + ../include/uibase/imodinterface.h + ../include/uibase/imodlist.h + ../include/uibase/imodrepositorybridge.h + ../include/uibase/imoinfo.h + ../include/uibase/iplugin.h + ../include/uibase/iplugindiagnose.h + ../include/uibase/ipluginfilemapper.h + ../include/uibase/iplugingame.h + ../include/uibase/iplugingamefeatures.h + ../include/uibase/iplugininstaller.h + ../include/uibase/iplugininstallercustom.h + ../include/uibase/iplugininstallersimple.h + ../include/uibase/ipluginlist.h + ../include/uibase/ipluginmodpage.h + ../include/uibase/ipluginpreview.h + ../include/uibase/ipluginproxy.h + ../include/uibase/iplugintool.h + ../include/uibase/iprofile.h + ../include/uibase/isavegame.h + ../include/uibase/isavegameinfowidget.h +) +set(tutorial_headers + ../include/uibase/tutorabledialog.h + ../include/uibase/tutorialcontrol.h + ../include/uibase/tutorialmanager.h +) +set(widget_headers + ../include/uibase/expanderwidget.h + ../include/uibase/filterwidget.h + ../include/uibase/finddialog.h + ../include/uibase/lineeditclear.h + ../include/uibase/linklabel.h + ../include/uibase/questionboxmemory.h + ../include/uibase/sortabletreewidget.h + ../include/uibase/taskprogressmanager.h + ../include/uibase/textviewer.h + ../include/uibase/widgetutility.h +) +set(game_features_header + ../include/uibase/game_features/bsainvalidation.h + ../include/uibase/game_features/dataarchives.h + ../include/uibase/game_features/game_feature.h + ../include/uibase/game_features/gameplugins.h + ../include/uibase/game_features/igamefeatures.h + ../include/uibase/game_features/localsavegames.h + ../include/uibase/game_features/moddatachecker.h + ../include/uibase/game_features/moddatacontent.h + ../include/uibase/game_features/savegameinfo.h + ../include/uibase/game_features/scriptextender.h + ../include/uibase/game_features/unmanagedmods.h +) +set(formatters_header + ../include/uibase/formatters/enums.h + ../include/uibase/formatters/qt.h + ../include/uibase/formatters/strings.h + ../include/uibase/formatters.h +) + +# Collect all source files +set(UIBASE_SOURCES + # root sources + delayedfilewriter.cpp + diagnosisreport.cpp + errorcodes.cpp + eventfilter.cpp + executableinfo.cpp + filesystemutilities.cpp + guessedvalue.cpp + json.cpp + log.cpp + modrepositoryfileinfo.cpp + nxmurl.cpp + pluginrequirements.cpp + registry.cpp + report.cpp + safewritefile.cpp + scopeguard.cpp + steamutility.cpp + strings.cpp + utility.cpp + versioning.cpp + versioninfo.cpp + # interfaces + ifiletree.cpp + imodrepositorybridge.cpp + imoinfo.cpp + # tutorials + tutorabledialog.cpp + tutorialcontrol.cpp + tutorialmanager.cpp + # widgets + expanderwidget.cpp + finddialog.cpp + lineeditclear.cpp + linklabel.cpp + questionboxmemory.cpp + sortabletreewidget.cpp + taskprogressmanager.cpp + textviewer.cpp + widgetutility.cpp + filterwidget.cpp +) + +add_library(uibase SHARED + ${UIBASE_SOURCES} + ${root_headers} + ${interface_headers} + ${tutorial_headers} + ${widget_headers} + ${game_features_header} + ${formatters_header} + pch.h + finddialog.ui + questionboxmemory.ui + taskdialog.ui + textviewer.ui +) + +set_target_properties(uibase PROPERTIES + DEBUG_POSTFIX d + AUTOMOC ON + AUTOUIC ON + CXX_STANDARD 23 + CXX_STANDARD_REQUIRED ON + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON + POSITION_INDEPENDENT_CODE ON +) + +target_include_directories(uibase + PUBLIC + $ + PRIVATE + $ + $ +) + +add_library(mo2::uibase ALIAS uibase) + +target_compile_definitions(uibase PRIVATE UIBASE_EXPORT SPDLOG_USE_STD_FORMAT) + +target_link_libraries(uibase + PUBLIC Qt6::Widgets Qt6::Network Qt6::QuickWidgets + PRIVATE spdlog::spdlog_header_only Qt6::Qml Qt6::Quick) + +# installation +install(TARGETS uibase EXPORT uibaseTargets + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin +) +install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/uibase DESTINATION include) +install(EXPORT uibaseTargets + FILE mo2-uibase-targets.cmake + NAMESPACE mo2:: + DESTINATION lib/cmake/mo2-uibase +) diff --git a/libs/uibase/src/delayedfilewriter.cpp b/libs/uibase/src/delayedfilewriter.cpp new file mode 100644 index 0000000..9d4703a --- /dev/null +++ b/libs/uibase/src/delayedfilewriter.cpp @@ -0,0 +1,49 @@ +#include +#include + +using namespace MOBase; + +DelayedFileWriterBase::DelayedFileWriterBase(int delay) : m_TimerDelay(delay), m_Timer() +{ + QObject::connect(&m_Timer, &QTimer::timeout, this, &DelayedFileWriter::timerExpired); + m_Timer.setSingleShot(true); +} + +DelayedFileWriterBase::~DelayedFileWriterBase() +{ + if (m_Timer.isActive()) { + log::error("delayed file save timer active at shutdown"); + } +} + +void DelayedFileWriterBase::write() +{ + m_Timer.start(m_TimerDelay); +} + +void DelayedFileWriterBase::cancel() +{ + m_Timer.stop(); +} + +void DelayedFileWriterBase::writeImmediately(bool ifOnTimer) +{ + if (!ifOnTimer || m_Timer.isActive()) { + m_Timer.stop(); + doWrite(); + } +} + +void DelayedFileWriterBase::timerExpired() +{ + doWrite(); +} + +DelayedFileWriter::DelayedFileWriter(DelayedFileWriter::WriterFunc func, int delay) + : DelayedFileWriterBase(delay), m_Func(func) +{} + +void DelayedFileWriter::doWrite() +{ + m_Func(); +} diff --git a/libs/uibase/src/diagnosisreport.cpp b/libs/uibase/src/diagnosisreport.cpp new file mode 100644 index 0000000..8a19a55 --- /dev/null +++ b/libs/uibase/src/diagnosisreport.cpp @@ -0,0 +1,25 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 + +namespace MOBase +{ +} // namespace MOBase diff --git a/libs/uibase/src/errorcodes.cpp b/libs/uibase/src/errorcodes.cpp new file mode 100644 index 0000000..16c262a --- /dev/null +++ b/libs/uibase/src/errorcodes.cpp @@ -0,0 +1,2361 @@ +#include +#include +#include + +namespace MOBase +{ + +struct CodeName +{ + DWORD code = 0; + const wchar_t* name = nullptr; +}; + +static const CodeName g_codes[] = { + {1, L"ERROR_INVALID_FUNCTION"}, + {2, L"ERROR_FILE_NOT_FOUND"}, + {3, L"ERROR_PATH_NOT_FOUND"}, + {4, L"ERROR_TOO_MANY_OPEN_FILES"}, + {5, L"ERROR_ACCESS_DENIED"}, + {6, L"ERROR_INVALID_HANDLE"}, + {7, L"ERROR_ARENA_TRASHED"}, + {8, L"ERROR_NOT_ENOUGH_MEMORY"}, + {9, L"ERROR_INVALID_BLOCK"}, + {10, L"ERROR_BAD_ENVIRONMENT"}, + {11, L"ERROR_BAD_FORMAT"}, + {12, L"ERROR_INVALID_ACCESS"}, + {13, L"ERROR_INVALID_DATA"}, + {14, L"ERROR_OUTOFMEMORY"}, + {15, L"ERROR_INVALID_DRIVE"}, + {16, L"ERROR_CURRENT_DIRECTORY"}, + {17, L"ERROR_NOT_SAME_DEVICE"}, + {18, L"ERROR_NO_MORE_FILES"}, + {19, L"ERROR_WRITE_PROTECT"}, + {20, L"ERROR_BAD_UNIT"}, + {21, L"ERROR_NOT_READY"}, + {22, L"ERROR_BAD_COMMAND"}, + {23, L"ERROR_CRC"}, + {24, L"ERROR_BAD_LENGTH"}, + {25, L"ERROR_SEEK"}, + {26, L"ERROR_NOT_DOS_DISK"}, + {27, L"ERROR_SECTOR_NOT_FOUND"}, + {28, L"ERROR_OUT_OF_PAPER"}, + {29, L"ERROR_WRITE_FAULT"}, + {30, L"ERROR_READ_FAULT"}, + {31, L"ERROR_GEN_FAILURE"}, + {32, L"ERROR_SHARING_VIOLATION"}, + {33, L"ERROR_LOCK_VIOLATION"}, + {34, L"ERROR_WRONG_DISK"}, + {36, L"ERROR_SHARING_BUFFER_EXCEEDED"}, + {38, L"ERROR_HANDLE_EOF"}, + {39, L"ERROR_HANDLE_DISK_FULL"}, + {50, L"ERROR_NOT_SUPPORTED"}, + {51, L"ERROR_REM_NOT_LIST"}, + {52, L"ERROR_DUP_NAME"}, + {53, L"ERROR_BAD_NETPATH"}, + {54, L"ERROR_NETWORK_BUSY"}, + {55, L"ERROR_DEV_NOT_EXIST"}, + {56, L"ERROR_TOO_MANY_CMDS"}, + {57, L"ERROR_ADAP_HDW_ERR"}, + {58, L"ERROR_BAD_NET_RESP"}, + {59, L"ERROR_UNEXP_NET_ERR"}, + {60, L"ERROR_BAD_REM_ADAP"}, + {61, L"ERROR_PRINTQ_FULL"}, + {62, L"ERROR_NO_SPOOL_SPACE"}, + {63, L"ERROR_PRINT_CANCELLED"}, + {64, L"ERROR_NETNAME_DELETED"}, + {65, L"ERROR_NETWORK_ACCESS_DENIED"}, + {66, L"ERROR_BAD_DEV_TYPE"}, + {67, L"ERROR_BAD_NET_NAME"}, + {68, L"ERROR_TOO_MANY_NAMES"}, + {69, L"ERROR_TOO_MANY_SESS"}, + {70, L"ERROR_SHARING_PAUSED"}, + {71, L"ERROR_REQ_NOT_ACCEP"}, + {72, L"ERROR_REDIR_PAUSED"}, + {80, L"ERROR_FILE_EXISTS"}, + {82, L"ERROR_CANNOT_MAKE"}, + {83, L"ERROR_FAIL_I24"}, + {84, L"ERROR_OUT_OF_STRUCTURES"}, + {85, L"ERROR_ALREADY_ASSIGNED"}, + {86, L"ERROR_INVALID_PASSWORD"}, + {87, L"ERROR_INVALID_PARAMETER"}, + {88, L"ERROR_NET_WRITE_FAULT"}, + {89, L"ERROR_NO_PROC_SLOTS"}, + {100, L"ERROR_TOO_MANY_SEMAPHORES"}, + {101, L"ERROR_EXCL_SEM_ALREADY_OWNED"}, + {102, L"ERROR_SEM_IS_SET"}, + {103, L"ERROR_TOO_MANY_SEM_REQUESTS"}, + {104, L"ERROR_INVALID_AT_INTERRUPT_TIME"}, + {105, L"ERROR_SEM_OWNER_DIED"}, + {106, L"ERROR_SEM_USER_LIMIT"}, + {107, L"ERROR_DISK_CHANGE"}, + {108, L"ERROR_DRIVE_LOCKED"}, + {109, L"ERROR_BROKEN_PIPE"}, + {110, L"ERROR_OPEN_FAILED"}, + {111, L"ERROR_BUFFER_OVERFLOW"}, + {112, L"ERROR_DISK_FULL"}, + {113, L"ERROR_NO_MORE_SEARCH_HANDLES"}, + {114, L"ERROR_INVALID_TARGET_HANDLE"}, + {117, L"ERROR_INVALID_CATEGORY"}, + {118, L"ERROR_INVALID_VERIFY_SWITCH"}, + {119, L"ERROR_BAD_DRIVER_LEVEL"}, + {120, L"ERROR_CALL_NOT_IMPLEMENTED"}, + {121, L"ERROR_SEM_TIMEOUT"}, + {122, L"ERROR_INSUFFICIENT_BUFFER"}, + {123, L"ERROR_INVALID_NAME"}, + {124, L"ERROR_INVALID_LEVEL"}, + {125, L"ERROR_NO_VOLUME_LABEL"}, + {126, L"ERROR_MOD_NOT_FOUND"}, + {127, L"ERROR_PROC_NOT_FOUND"}, + {128, L"ERROR_WAIT_NO_CHILDREN"}, + {129, L"ERROR_CHILD_NOT_COMPLETE"}, + {130, L"ERROR_DIRECT_ACCESS_HANDLE"}, + {131, L"ERROR_NEGATIVE_SEEK"}, + {132, L"ERROR_SEEK_ON_DEVICE"}, + {133, L"ERROR_IS_JOIN_TARGET"}, + {134, L"ERROR_IS_JOINED"}, + {135, L"ERROR_IS_SUBSTED"}, + {136, L"ERROR_NOT_JOINED"}, + {137, L"ERROR_NOT_SUBSTED"}, + {138, L"ERROR_JOIN_TO_JOIN"}, + {139, L"ERROR_SUBST_TO_SUBST"}, + {140, L"ERROR_JOIN_TO_SUBST"}, + {141, L"ERROR_SUBST_TO_JOIN"}, + {142, L"ERROR_BUSY_DRIVE"}, + {143, L"ERROR_SAME_DRIVE"}, + {144, L"ERROR_DIR_NOT_ROOT"}, + {145, L"ERROR_DIR_NOT_EMPTY"}, + {146, L"ERROR_IS_SUBST_PATH"}, + {147, L"ERROR_IS_JOIN_PATH"}, + {148, L"ERROR_PATH_BUSY"}, + {149, L"ERROR_IS_SUBST_TARGET"}, + {150, L"ERROR_SYSTEM_TRACE"}, + {151, L"ERROR_INVALID_EVENT_COUNT"}, + {152, L"ERROR_TOO_MANY_MUXWAITERS"}, + {153, L"ERROR_INVALID_LIST_FORMAT"}, + {154, L"ERROR_LABEL_TOO_LONG"}, + {155, L"ERROR_TOO_MANY_TCBS"}, + {156, L"ERROR_SIGNAL_REFUSED"}, + {157, L"ERROR_DISCARDED"}, + {158, L"ERROR_NOT_LOCKED"}, + {159, L"ERROR_BAD_THREADID_ADDR"}, + {160, L"ERROR_BAD_ARGUMENTS"}, + {161, L"ERROR_BAD_PATHNAME"}, + {162, L"ERROR_SIGNAL_PENDING"}, + {164, L"ERROR_MAX_THRDS_REACHED"}, + {167, L"ERROR_LOCK_FAILED"}, + {170, L"ERROR_BUSY"}, + {171, L"ERROR_DEVICE_SUPPORT_IN_PROGRESS"}, + {173, L"ERROR_CANCEL_VIOLATION"}, + {174, L"ERROR_ATOMIC_LOCKS_NOT_SUPPORTED"}, + {180, L"ERROR_INVALID_SEGMENT_NUMBER"}, + {182, L"ERROR_INVALID_ORDINAL"}, + {183, L"ERROR_ALREADY_EXISTS"}, + {186, L"ERROR_INVALID_FLAG_NUMBER"}, + {187, L"ERROR_SEM_NOT_FOUND"}, + {188, L"ERROR_INVALID_STARTING_CODESEG"}, + {189, L"ERROR_INVALID_STACKSEG"}, + {190, L"ERROR_INVALID_MODULETYPE"}, + {191, L"ERROR_INVALID_EXE_SIGNATURE"}, + {192, L"ERROR_EXE_MARKED_INVALID"}, + {193, L"ERROR_BAD_EXE_FORMAT"}, + {194, L"ERROR_ITERATED_DATA_EXCEEDS_64k"}, + {195, L"ERROR_INVALID_MINALLOCSIZE"}, + {196, L"ERROR_DYNLINK_FROM_INVALID_RING"}, + {197, L"ERROR_IOPL_NOT_ENABLED"}, + {198, L"ERROR_INVALID_SEGDPL"}, + {199, L"ERROR_AUTODATASEG_EXCEEDS_64k"}, + {200, L"ERROR_RING2SEG_MUST_BE_MOVABLE"}, + {201, L"ERROR_RELOC_CHAIN_XEEDS_SEGLIM"}, + {202, L"ERROR_INFLOOP_IN_RELOC_CHAIN"}, + {203, L"ERROR_ENVVAR_NOT_FOUND"}, + {205, L"ERROR_NO_SIGNAL_SENT"}, + {206, L"ERROR_FILENAME_EXCED_RANGE"}, + {207, L"ERROR_RING2_STACK_IN_USE"}, + {208, L"ERROR_META_EXPANSION_TOO_LONG"}, + {209, L"ERROR_INVALID_SIGNAL_NUMBER"}, + {210, L"ERROR_THREAD_1_INACTIVE"}, + {212, L"ERROR_LOCKED"}, + {214, L"ERROR_TOO_MANY_MODULES"}, + {215, L"ERROR_NESTING_NOT_ALLOWED"}, + {216, L"ERROR_EXE_MACHINE_TYPE_MISMATCH"}, + {217, L"ERROR_EXE_CANNOT_MODIFY_SIGNED_BINARY"}, + {218, L"ERROR_EXE_CANNOT_MODIFY_STRONG_SIGNED_BINARY"}, + {220, L"ERROR_FILE_CHECKED_OUT"}, + {221, L"ERROR_CHECKOUT_REQUIRED"}, + {222, L"ERROR_BAD_FILE_TYPE"}, + {223, L"ERROR_FILE_TOO_LARGE"}, + {224, L"ERROR_FORMS_AUTH_REQUIRED"}, + {225, L"ERROR_VIRUS_INFECTED"}, + {226, L"ERROR_VIRUS_DELETED"}, + {229, L"ERROR_PIPE_LOCAL"}, + {230, L"ERROR_BAD_PIPE"}, + {231, L"ERROR_PIPE_BUSY"}, + {232, L"ERROR_NO_DATA"}, + {233, L"ERROR_PIPE_NOT_CONNECTED"}, + {234, L"ERROR_MORE_DATA"}, + {240, L"ERROR_VC_DISCONNECTED"}, + {254, L"ERROR_INVALID_EA_NAME"}, + {255, L"ERROR_EA_LIST_INCONSISTENT"}, + {258, L"WAIT_TIMEOUT"}, + {259, L"ERROR_NO_MORE_ITEMS"}, + {266, L"ERROR_CANNOT_COPY"}, + {267, L"ERROR_DIRECTORY"}, + {275, L"ERROR_EAS_DIDNT_FIT"}, + {276, L"ERROR_EA_FILE_CORRUPT"}, + {277, L"ERROR_EA_TABLE_FULL"}, + {278, L"ERROR_INVALID_EA_HANDLE"}, + {282, L"ERROR_EAS_NOT_SUPPORTED"}, + {288, L"ERROR_NOT_OWNER"}, + {298, L"ERROR_TOO_MANY_POSTS"}, + {299, L"ERROR_PARTIAL_COPY"}, + {300, L"ERROR_OPLOCK_NOT_GRANTED"}, + {301, L"ERROR_INVALID_OPLOCK_PROTOCOL"}, + {302, L"ERROR_DISK_TOO_FRAGMENTED"}, + {303, L"ERROR_DELETE_PENDING"}, + {304, L"ERROR_INCOMPATIBLE_WITH_GLOBAL_SHORT_NAME_REGISTRY_SETTING"}, + {305, L"ERROR_SHORT_NAMES_NOT_ENABLED_ON_VOLUME"}, + {306, L"ERROR_SECURITY_STREAM_IS_INCONSISTENT"}, + {307, L"ERROR_INVALID_LOCK_RANGE"}, + {308, L"ERROR_IMAGE_SUBSYSTEM_NOT_PRESENT"}, + {309, L"ERROR_NOTIFICATION_GUID_ALREADY_DEFINED"}, + {310, L"ERROR_INVALID_EXCEPTION_HANDLER"}, + {311, L"ERROR_DUPLICATE_PRIVILEGES"}, + {312, L"ERROR_NO_RANGES_PROCESSED"}, + {313, L"ERROR_NOT_ALLOWED_ON_SYSTEM_FILE"}, + {314, L"ERROR_DISK_RESOURCES_EXHAUSTED"}, + {315, L"ERROR_INVALID_TOKEN"}, + {316, L"ERROR_DEVICE_FEATURE_NOT_SUPPORTED"}, + {317, L"ERROR_MR_MID_NOT_FOUND"}, + {318, L"ERROR_SCOPE_NOT_FOUND"}, + {319, L"ERROR_UNDEFINED_SCOPE"}, + {320, L"ERROR_INVALID_CAP"}, + {321, L"ERROR_DEVICE_UNREACHABLE"}, + {322, L"ERROR_DEVICE_NO_RESOURCES"}, + {323, L"ERROR_DATA_CHECKSUM_ERROR"}, + {324, L"ERROR_INTERMIXED_KERNEL_EA_OPERATION"}, + {326, L"ERROR_FILE_LEVEL_TRIM_NOT_SUPPORTED"}, + {327, L"ERROR_OFFSET_ALIGNMENT_VIOLATION"}, + {328, L"ERROR_INVALID_FIELD_IN_PARAMETER_LIST"}, + {329, L"ERROR_OPERATION_IN_PROGRESS"}, + {330, L"ERROR_BAD_DEVICE_PATH"}, + {331, L"ERROR_TOO_MANY_DESCRIPTORS"}, + {332, L"ERROR_SCRUB_DATA_DISABLED"}, + {333, L"ERROR_NOT_REDUNDANT_STORAGE"}, + {334, L"ERROR_RESIDENT_FILE_NOT_SUPPORTED"}, + {335, L"ERROR_COMPRESSED_FILE_NOT_SUPPORTED"}, + {336, L"ERROR_DIRECTORY_NOT_SUPPORTED"}, + {337, L"ERROR_NOT_READ_FROM_COPY"}, + {350, L"ERROR_FAIL_NOACTION_REBOOT"}, + {351, L"ERROR_FAIL_SHUTDOWN"}, + {352, L"ERROR_FAIL_RESTART"}, + {353, L"ERROR_MAX_SESSIONS_REACHED"}, + {400, L"ERROR_THREAD_MODE_ALREADY_BACKGROUND"}, + {401, L"ERROR_THREAD_MODE_NOT_BACKGROUND"}, + {402, L"ERROR_PROCESS_MODE_ALREADY_BACKGROUND"}, + {403, L"ERROR_PROCESS_MODE_NOT_BACKGROUND"}, + {487, L"ERROR_INVALID_ADDRESS"}, + {500, L"ERROR_USER_PROFILE_LOAD"}, + {534, L"ERROR_ARITHMETIC_OVERFLOW"}, + {535, L"ERROR_PIPE_CONNECTED"}, + {536, L"ERROR_PIPE_LISTENING"}, + {537, L"ERROR_VERIFIER_STOP"}, + {538, L"ERROR_ABIOS_ERROR"}, + {539, L"ERROR_WX86_WARNING"}, + {540, L"ERROR_WX86_ERROR"}, + {541, L"ERROR_TIMER_NOT_CANCELED"}, + {542, L"ERROR_UNWIND"}, + {543, L"ERROR_BAD_STACK"}, + {544, L"ERROR_INVALID_UNWIND_TARGET"}, + {545, L"ERROR_INVALID_PORT_ATTRIBUTES"}, + {546, L"ERROR_PORT_MESSAGE_TOO_LONG"}, + {547, L"ERROR_INVALID_QUOTA_LOWER"}, + {548, L"ERROR_DEVICE_ALREADY_ATTACHED"}, + {549, L"ERROR_INSTRUCTION_MISALIGNMENT"}, + {550, L"ERROR_PROFILING_NOT_STARTED"}, + {551, L"ERROR_PROFILING_NOT_STOPPED"}, + {552, L"ERROR_COULD_NOT_INTERPRET"}, + {553, L"ERROR_PROFILING_AT_LIMIT"}, + {554, L"ERROR_CANT_WAIT"}, + {555, L"ERROR_CANT_TERMINATE_SELF"}, + {556, L"ERROR_UNEXPECTED_MM_CREATE_ERR"}, + {557, L"ERROR_UNEXPECTED_MM_MAP_ERROR"}, + {558, L"ERROR_UNEXPECTED_MM_EXTEND_ERR"}, + {559, L"ERROR_BAD_FUNCTION_TABLE"}, + {560, L"ERROR_NO_GUID_TRANSLATION"}, + {561, L"ERROR_INVALID_LDT_SIZE"}, + {563, L"ERROR_INVALID_LDT_OFFSET"}, + {564, L"ERROR_INVALID_LDT_DESCRIPTOR"}, + {565, L"ERROR_TOO_MANY_THREADS"}, + {566, L"ERROR_THREAD_NOT_IN_PROCESS"}, + {567, L"ERROR_PAGEFILE_QUOTA_EXCEEDED"}, + {568, L"ERROR_LOGON_SERVER_CONFLICT"}, + {569, L"ERROR_SYNCHRONIZATION_REQUIRED"}, + {570, L"ERROR_NET_OPEN_FAILED"}, + {571, L"ERROR_IO_PRIVILEGE_FAILED"}, + {572, L"ERROR_CONTROL_C_EXIT"}, + {573, L"ERROR_MISSING_SYSTEMFILE"}, + {574, L"ERROR_UNHANDLED_EXCEPTION"}, + {575, L"ERROR_APP_INIT_FAILURE"}, + {576, L"ERROR_PAGEFILE_CREATE_FAILED"}, + {577, L"ERROR_INVALID_IMAGE_HASH"}, + {578, L"ERROR_NO_PAGEFILE"}, + {579, L"ERROR_ILLEGAL_FLOAT_CONTEXT"}, + {580, L"ERROR_NO_EVENT_PAIR"}, + {581, L"ERROR_DOMAIN_CTRLR_CONFIG_ERROR"}, + {582, L"ERROR_ILLEGAL_CHARACTER"}, + {583, L"ERROR_UNDEFINED_CHARACTER"}, + {584, L"ERROR_FLOPPY_VOLUME"}, + {585, L"ERROR_BIOS_FAILED_TO_CONNECT_INTERRUPT"}, + {586, L"ERROR_BACKUP_CONTROLLER"}, + {587, L"ERROR_MUTANT_LIMIT_EXCEEDED"}, + {588, L"ERROR_FS_DRIVER_REQUIRED"}, + {589, L"ERROR_CANNOT_LOAD_REGISTRY_FILE"}, + {590, L"ERROR_DEBUG_ATTACH_FAILED"}, + {591, L"ERROR_SYSTEM_PROCESS_TERMINATED"}, + {592, L"ERROR_DATA_NOT_ACCEPTED"}, + {593, L"ERROR_VDM_HARD_ERROR"}, + {594, L"ERROR_DRIVER_CANCEL_TIMEOUT"}, + {595, L"ERROR_REPLY_MESSAGE_MISMATCH"}, + {596, L"ERROR_LOST_WRITEBEHIND_DATA"}, + {597, L"ERROR_CLIENT_SERVER_PARAMETERS_INVALID"}, + {598, L"ERROR_NOT_TINY_STREAM"}, + {599, L"ERROR_STACK_OVERFLOW_READ"}, + {600, L"ERROR_CONVERT_TO_LARGE"}, + {601, L"ERROR_FOUND_OUT_OF_SCOPE"}, + {602, L"ERROR_ALLOCATE_BUCKET"}, + {603, L"ERROR_MARSHALL_OVERFLOW"}, + {604, L"ERROR_INVALID_VARIANT"}, + {605, L"ERROR_BAD_COMPRESSION_BUFFER"}, + {606, L"ERROR_AUDIT_FAILED"}, + {607, L"ERROR_TIMER_RESOLUTION_NOT_SET"}, + {608, L"ERROR_INSUFFICIENT_LOGON_INFO"}, + {609, L"ERROR_BAD_DLL_ENTRYPOINT"}, + {610, L"ERROR_BAD_SERVICE_ENTRYPOINT"}, + {611, L"ERROR_IP_ADDRESS_CONFLICT1"}, + {612, L"ERROR_IP_ADDRESS_CONFLICT2"}, + {613, L"ERROR_REGISTRY_QUOTA_LIMIT"}, + {614, L"ERROR_NO_CALLBACK_ACTIVE"}, + {615, L"ERROR_PWD_TOO_SHORT"}, + {616, L"ERROR_PWD_TOO_RECENT"}, + {617, L"ERROR_PWD_HISTORY_CONFLICT"}, + {618, L"ERROR_UNSUPPORTED_COMPRESSION"}, + {619, L"ERROR_INVALID_HW_PROFILE"}, + {620, L"ERROR_INVALID_PLUGPLAY_DEVICE_PATH"}, + {621, L"ERROR_QUOTA_LIST_INCONSISTENT"}, + {622, L"ERROR_EVALUATION_EXPIRATION"}, + {623, L"ERROR_ILLEGAL_DLL_RELOCATION"}, + {624, L"ERROR_DLL_INIT_FAILED_LOGOFF"}, + {625, L"ERROR_VALIDATE_CONTINUE"}, + {626, L"ERROR_NO_MORE_MATCHES"}, + {627, L"ERROR_RANGE_LIST_CONFLICT"}, + {628, L"ERROR_SERVER_SID_MISMATCH"}, + {629, L"ERROR_CANT_ENABLE_DENY_ONLY"}, + {630, L"ERROR_FLOAT_MULTIPLE_FAULTS"}, + {631, L"ERROR_FLOAT_MULTIPLE_TRAPS"}, + {632, L"ERROR_NOINTERFACE"}, + {633, L"ERROR_DRIVER_FAILED_SLEEP"}, + {634, L"ERROR_CORRUPT_SYSTEM_FILE"}, + {635, L"ERROR_COMMITMENT_MINIMUM"}, + {636, L"ERROR_PNP_RESTART_ENUMERATION"}, + {637, L"ERROR_SYSTEM_IMAGE_BAD_SIGNATURE"}, + {638, L"ERROR_PNP_REBOOT_REQUIRED"}, + {639, L"ERROR_INSUFFICIENT_POWER"}, + {640, L"ERROR_MULTIPLE_FAULT_VIOLATION"}, + {641, L"ERROR_SYSTEM_SHUTDOWN"}, + {642, L"ERROR_PORT_NOT_SET"}, + {643, L"ERROR_DS_VERSION_CHECK_FAILURE"}, + {644, L"ERROR_RANGE_NOT_FOUND"}, + {646, L"ERROR_NOT_SAFE_MODE_DRIVER"}, + {647, L"ERROR_FAILED_DRIVER_ENTRY"}, + {648, L"ERROR_DEVICE_ENUMERATION_ERROR"}, + {649, L"ERROR_MOUNT_POINT_NOT_RESOLVED"}, + {650, L"ERROR_INVALID_DEVICE_OBJECT_PARAMETER"}, + {651, L"ERROR_MCA_OCCURED"}, + {652, L"ERROR_DRIVER_DATABASE_ERROR"}, + {653, L"ERROR_SYSTEM_HIVE_TOO_LARGE"}, + {654, L"ERROR_DRIVER_FAILED_PRIOR_UNLOAD"}, + {655, L"ERROR_VOLSNAP_PREPARE_HIBERNATE"}, + {656, L"ERROR_HIBERNATION_FAILURE"}, + {657, L"ERROR_PWD_TOO_LONG"}, + {665, L"ERROR_FILE_SYSTEM_LIMITATION"}, + {668, L"ERROR_ASSERTION_FAILURE"}, + {669, L"ERROR_ACPI_ERROR"}, + {670, L"ERROR_WOW_ASSERTION"}, + {671, L"ERROR_PNP_BAD_MPS_TABLE"}, + {672, L"ERROR_PNP_TRANSLATION_FAILED"}, + {673, L"ERROR_PNP_IRQ_TRANSLATION_FAILED"}, + {674, L"ERROR_PNP_INVALID_ID"}, + {675, L"ERROR_WAKE_SYSTEM_DEBUGGER"}, + {676, L"ERROR_HANDLES_CLOSED"}, + {677, L"ERROR_EXTRANEOUS_INFORMATION"}, + {678, L"ERROR_RXACT_COMMIT_NECESSARY"}, + {679, L"ERROR_MEDIA_CHECK"}, + {680, L"ERROR_GUID_SUBSTITUTION_MADE"}, + {681, L"ERROR_STOPPED_ON_SYMLINK"}, + {682, L"ERROR_LONGJUMP"}, + {683, L"ERROR_PLUGPLAY_QUERY_VETOED"}, + {684, L"ERROR_UNWIND_CONSOLIDATE"}, + {685, L"ERROR_REGISTRY_HIVE_RECOVERED"}, + {686, L"ERROR_DLL_MIGHT_BE_INSECURE"}, + {687, L"ERROR_DLL_MIGHT_BE_INCOMPATIBLE"}, + {688, L"ERROR_DBG_EXCEPTION_NOT_HANDLED"}, + {689, L"ERROR_DBG_REPLY_LATER"}, + {690, L"ERROR_DBG_UNABLE_TO_PROVIDE_HANDLE"}, + {691, L"ERROR_DBG_TERMINATE_THREAD"}, + {692, L"ERROR_DBG_TERMINATE_PROCESS"}, + {693, L"ERROR_DBG_CONTROL_C"}, + {694, L"ERROR_DBG_PRINTEXCEPTION_C"}, + {695, L"ERROR_DBG_RIPEXCEPTION"}, + {696, L"ERROR_DBG_CONTROL_BREAK"}, + {697, L"ERROR_DBG_COMMAND_EXCEPTION"}, + {698, L"ERROR_OBJECT_NAME_EXISTS"}, + {699, L"ERROR_THREAD_WAS_SUSPENDED"}, + {700, L"ERROR_IMAGE_NOT_AT_BASE"}, + {701, L"ERROR_RXACT_STATE_CREATED"}, + {702, L"ERROR_SEGMENT_NOTIFICATION"}, + {703, L"ERROR_BAD_CURRENT_DIRECTORY"}, + {704, L"ERROR_FT_READ_RECOVERY_FROM_BACKUP"}, + {705, L"ERROR_FT_WRITE_RECOVERY"}, + {706, L"ERROR_IMAGE_MACHINE_TYPE_MISMATCH"}, + {707, L"ERROR_RECEIVE_PARTIAL"}, + {708, L"ERROR_RECEIVE_EXPEDITED"}, + {709, L"ERROR_RECEIVE_PARTIAL_EXPEDITED"}, + {710, L"ERROR_EVENT_DONE"}, + {711, L"ERROR_EVENT_PENDING"}, + {712, L"ERROR_CHECKING_FILE_SYSTEM"}, + {713, L"ERROR_FATAL_APP_EXIT"}, + {714, L"ERROR_PREDEFINED_HANDLE"}, + {715, L"ERROR_WAS_UNLOCKED"}, + {716, L"ERROR_SERVICE_NOTIFICATION"}, + {717, L"ERROR_WAS_LOCKED"}, + {718, L"ERROR_LOG_HARD_ERROR"}, + {719, L"ERROR_ALREADY_WIN32"}, + {720, L"ERROR_IMAGE_MACHINE_TYPE_MISMATCH_EXE"}, + {721, L"ERROR_NO_YIELD_PERFORMED"}, + {722, L"ERROR_TIMER_RESUME_IGNORED"}, + {723, L"ERROR_ARBITRATION_UNHANDLED"}, + {724, L"ERROR_CARDBUS_NOT_SUPPORTED"}, + {725, L"ERROR_MP_PROCESSOR_MISMATCH"}, + {726, L"ERROR_HIBERNATED"}, + {727, L"ERROR_RESUME_HIBERNATION"}, + {728, L"ERROR_FIRMWARE_UPDATED"}, + {729, L"ERROR_DRIVERS_LEAKING_LOCKED_PAGES"}, + {730, L"ERROR_WAKE_SYSTEM"}, + {731, L"ERROR_WAIT_1"}, + {732, L"ERROR_WAIT_2"}, + {733, L"ERROR_WAIT_3"}, + {734, L"ERROR_WAIT_63"}, + {735, L"ERROR_ABANDONED_WAIT_0"}, + {736, L"ERROR_ABANDONED_WAIT_63"}, + {737, L"ERROR_USER_APC"}, + {738, L"ERROR_KERNEL_APC"}, + {739, L"ERROR_ALERTED"}, + {740, L"ERROR_ELEVATION_REQUIRED"}, + {741, L"ERROR_REPARSE"}, + {742, L"ERROR_OPLOCK_BREAK_IN_PROGRESS"}, + {743, L"ERROR_VOLUME_MOUNTED"}, + {744, L"ERROR_RXACT_COMMITTED"}, + {745, L"ERROR_NOTIFY_CLEANUP"}, + {746, L"ERROR_PRIMARY_TRANSPORT_CONNECT_FAILED"}, + {747, L"ERROR_PAGE_FAULT_TRANSITION"}, + {748, L"ERROR_PAGE_FAULT_DEMAND_ZERO"}, + {749, L"ERROR_PAGE_FAULT_COPY_ON_WRITE"}, + {750, L"ERROR_PAGE_FAULT_GUARD_PAGE"}, + {751, L"ERROR_PAGE_FAULT_PAGING_FILE"}, + {752, L"ERROR_CACHE_PAGE_LOCKED"}, + {753, L"ERROR_CRASH_DUMP"}, + {754, L"ERROR_BUFFER_ALL_ZEROS"}, + {755, L"ERROR_REPARSE_OBJECT"}, + {756, L"ERROR_RESOURCE_REQUIREMENTS_CHANGED"}, + {757, L"ERROR_TRANSLATION_COMPLETE"}, + {758, L"ERROR_NOTHING_TO_TERMINATE"}, + {759, L"ERROR_PROCESS_NOT_IN_JOB"}, + {760, L"ERROR_PROCESS_IN_JOB"}, + {761, L"ERROR_VOLSNAP_HIBERNATE_READY"}, + {762, L"ERROR_FSFILTER_OP_COMPLETED_SUCCESSFULLY"}, + {763, L"ERROR_INTERRUPT_VECTOR_ALREADY_CONNECTED"}, + {764, L"ERROR_INTERRUPT_STILL_CONNECTED"}, + {765, L"ERROR_WAIT_FOR_OPLOCK"}, + {766, L"ERROR_DBG_EXCEPTION_HANDLED"}, + {767, L"ERROR_DBG_CONTINUE"}, + {768, L"ERROR_CALLBACK_POP_STACK"}, + {769, L"ERROR_COMPRESSION_DISABLED"}, + {770, L"ERROR_CANTFETCHBACKWARDS"}, + {771, L"ERROR_CANTSCROLLBACKWARDS"}, + {772, L"ERROR_ROWSNOTRELEASED"}, + {773, L"ERROR_BAD_ACCESSOR_FLAGS"}, + {774, L"ERROR_ERRORS_ENCOUNTERED"}, + {775, L"ERROR_NOT_CAPABLE"}, + {776, L"ERROR_REQUEST_OUT_OF_SEQUENCE"}, + {777, L"ERROR_VERSION_PARSE_ERROR"}, + {778, L"ERROR_BADSTARTPOSITION"}, + {779, L"ERROR_MEMORY_HARDWARE"}, + {780, L"ERROR_DISK_REPAIR_DISABLED"}, + {781, L"ERROR_INSUFFICIENT_RESOURCE_FOR_SPECIFIED_SHARED_SECTION_SIZE"}, + {782, L"ERROR_SYSTEM_POWERSTATE_TRANSITION"}, + {783, L"ERROR_SYSTEM_POWERSTATE_COMPLEX_TRANSITION"}, + {784, L"ERROR_MCA_EXCEPTION"}, + {785, L"ERROR_ACCESS_AUDIT_BY_POLICY"}, + {786, L"ERROR_ACCESS_DISABLED_NO_SAFER_UI_BY_POLICY"}, + {787, L"ERROR_ABANDON_HIBERFILE"}, + {788, L"ERROR_LOST_WRITEBEHIND_DATA_NETWORK_DISCONNECTED"}, + {789, L"ERROR_LOST_WRITEBEHIND_DATA_NETWORK_SERVER_ERROR"}, + {790, L"ERROR_LOST_WRITEBEHIND_DATA_LOCAL_DISK_ERROR"}, + {791, L"ERROR_BAD_MCFG_TABLE"}, + {792, L"ERROR_DISK_REPAIR_REDIRECTED"}, + {793, L"ERROR_DISK_REPAIR_UNSUCCESSFUL"}, + {794, L"ERROR_CORRUPT_LOG_OVERFULL"}, + {795, L"ERROR_CORRUPT_LOG_CORRUPTED"}, + {796, L"ERROR_CORRUPT_LOG_UNAVAILABLE"}, + {797, L"ERROR_CORRUPT_LOG_DELETED_FULL"}, + {798, L"ERROR_CORRUPT_LOG_CLEARED"}, + {799, L"ERROR_ORPHAN_NAME_EXHAUSTED"}, + {800, L"ERROR_OPLOCK_SWITCHED_TO_NEW_HANDLE"}, + {801, L"ERROR_CANNOT_GRANT_REQUESTED_OPLOCK"}, + {802, L"ERROR_CANNOT_BREAK_OPLOCK"}, + {803, L"ERROR_OPLOCK_HANDLE_CLOSED"}, + {804, L"ERROR_NO_ACE_CONDITION"}, + {805, L"ERROR_INVALID_ACE_CONDITION"}, + {806, L"ERROR_FILE_HANDLE_REVOKED"}, + {807, L"ERROR_IMAGE_AT_DIFFERENT_BASE"}, + {994, L"ERROR_EA_ACCESS_DENIED"}, + {995, L"ERROR_OPERATION_ABORTED"}, + {996, L"ERROR_IO_INCOMPLETE"}, + {997, L"ERROR_IO_PENDING"}, + {998, L"ERROR_NOACCESS"}, + {999, L"ERROR_SWAPERROR"}, + {1001, L"ERROR_STACK_OVERFLOW"}, + {1002, L"ERROR_INVALID_MESSAGE"}, + {1003, L"ERROR_CAN_NOT_COMPLETE"}, + {1004, L"ERROR_INVALID_FLAGS"}, + {1005, L"ERROR_UNRECOGNIZED_VOLUME"}, + {1006, L"ERROR_FILE_INVALID"}, + {1007, L"ERROR_FULLSCREEN_MODE"}, + {1008, L"ERROR_NO_TOKEN"}, + {1009, L"ERROR_BADDB"}, + {1010, L"ERROR_BADKEY"}, + {1011, L"ERROR_CANTOPEN"}, + {1012, L"ERROR_CANTREAD"}, + {1013, L"ERROR_CANTWRITE"}, + {1014, L"ERROR_REGISTRY_RECOVERED"}, + {1015, L"ERROR_REGISTRY_CORRUPT"}, + {1016, L"ERROR_REGISTRY_IO_FAILED"}, + {1017, L"ERROR_NOT_REGISTRY_FILE"}, + {1018, L"ERROR_KEY_DELETED"}, + {1019, L"ERROR_NO_LOG_SPACE"}, + {1020, L"ERROR_KEY_HAS_CHILDREN"}, + {1021, L"ERROR_CHILD_MUST_BE_VOLATILE"}, + {1022, L"ERROR_NOTIFY_ENUM_DIR"}, + {1051, L"ERROR_DEPENDENT_SERVICES_RUNNING"}, + {1052, L"ERROR_INVALID_SERVICE_CONTROL"}, + {1053, L"ERROR_SERVICE_REQUEST_TIMEOUT"}, + {1054, L"ERROR_SERVICE_NO_THREAD"}, + {1055, L"ERROR_SERVICE_DATABASE_LOCKED"}, + {1056, L"ERROR_SERVICE_ALREADY_RUNNING"}, + {1057, L"ERROR_INVALID_SERVICE_ACCOUNT"}, + {1058, L"ERROR_SERVICE_DISABLED"}, + {1059, L"ERROR_CIRCULAR_DEPENDENCY"}, + {1060, L"ERROR_SERVICE_DOES_NOT_EXIST"}, + {1061, L"ERROR_SERVICE_CANNOT_ACCEPT_CTRL"}, + {1062, L"ERROR_SERVICE_NOT_ACTIVE"}, + {1063, L"ERROR_FAILED_SERVICE_CONTROLLER_CONNECT"}, + {1064, L"ERROR_EXCEPTION_IN_SERVICE"}, + {1065, L"ERROR_DATABASE_DOES_NOT_EXIST"}, + {1066, L"ERROR_SERVICE_SPECIFIC_ERROR"}, + {1067, L"ERROR_PROCESS_ABORTED"}, + {1068, L"ERROR_SERVICE_DEPENDENCY_FAIL"}, + {1069, L"ERROR_SERVICE_LOGON_FAILED"}, + {1070, L"ERROR_SERVICE_START_HANG"}, + {1071, L"ERROR_INVALID_SERVICE_LOCK"}, + {1072, L"ERROR_SERVICE_MARKED_FOR_DELETE"}, + {1073, L"ERROR_SERVICE_EXISTS"}, + {1074, L"ERROR_ALREADY_RUNNING_LKG"}, + {1075, L"ERROR_SERVICE_DEPENDENCY_DELETED"}, + {1076, L"ERROR_BOOT_ALREADY_ACCEPTED"}, + {1077, L"ERROR_SERVICE_NEVER_STARTED"}, + {1078, L"ERROR_DUPLICATE_SERVICE_NAME"}, + {1079, L"ERROR_DIFFERENT_SERVICE_ACCOUNT"}, + {1080, L"ERROR_CANNOT_DETECT_DRIVER_FAILURE"}, + {1081, L"ERROR_CANNOT_DETECT_PROCESS_ABORT"}, + {1082, L"ERROR_NO_RECOVERY_PROGRAM"}, + {1083, L"ERROR_SERVICE_NOT_IN_EXE"}, + {1084, L"ERROR_NOT_SAFEBOOT_SERVICE"}, + {1100, L"ERROR_END_OF_MEDIA"}, + {1101, L"ERROR_FILEMARK_DETECTED"}, + {1102, L"ERROR_BEGINNING_OF_MEDIA"}, + {1103, L"ERROR_SETMARK_DETECTED"}, + {1104, L"ERROR_NO_DATA_DETECTED"}, + {1105, L"ERROR_PARTITION_FAILURE"}, + {1106, L"ERROR_INVALID_BLOCK_LENGTH"}, + {1107, L"ERROR_DEVICE_NOT_PARTITIONED"}, + {1108, L"ERROR_UNABLE_TO_LOCK_MEDIA"}, + {1109, L"ERROR_UNABLE_TO_UNLOAD_MEDIA"}, + {1110, L"ERROR_MEDIA_CHANGED"}, + {1111, L"ERROR_BUS_RESET"}, + {1112, L"ERROR_NO_MEDIA_IN_DRIVE"}, + {1113, L"ERROR_NO_UNICODE_TRANSLATION"}, + {1114, L"ERROR_DLL_INIT_FAILED"}, + {1115, L"ERROR_SHUTDOWN_IN_PROGRESS"}, + {1116, L"ERROR_NO_SHUTDOWN_IN_PROGRESS"}, + {1117, L"ERROR_IO_DEVICE"}, + {1118, L"ERROR_SERIAL_NO_DEVICE"}, + {1119, L"ERROR_IRQ_BUSY"}, + {1120, L"ERROR_MORE_WRITES"}, + {1121, L"ERROR_COUNTER_TIMEOUT"}, + {1122, L"ERROR_FLOPPY_ID_MARK_NOT_FOUND"}, + {1123, L"ERROR_FLOPPY_WRONG_CYLINDER"}, + {1124, L"ERROR_FLOPPY_UNKNOWN_ERROR"}, + {1125, L"ERROR_FLOPPY_BAD_REGISTERS"}, + {1126, L"ERROR_DISK_RECALIBRATE_FAILED"}, + {1127, L"ERROR_DISK_OPERATION_FAILED"}, + {1128, L"ERROR_DISK_RESET_FAILED"}, + {1129, L"ERROR_EOM_OVERFLOW"}, + {1130, L"ERROR_NOT_ENOUGH_SERVER_MEMORY"}, + {1131, L"ERROR_POSSIBLE_DEADLOCK"}, + {1132, L"ERROR_MAPPED_ALIGNMENT"}, + {1140, L"ERROR_SET_POWER_STATE_VETOED"}, + {1141, L"ERROR_SET_POWER_STATE_FAILED"}, + {1142, L"ERROR_TOO_MANY_LINKS"}, + {1150, L"ERROR_OLD_WIN_VERSION"}, + {1151, L"ERROR_APP_WRONG_OS"}, + {1152, L"ERROR_SINGLE_INSTANCE_APP"}, + {1153, L"ERROR_RMODE_APP"}, + {1154, L"ERROR_INVALID_DLL"}, + {1155, L"ERROR_NO_ASSOCIATION"}, + {1156, L"ERROR_DDE_FAIL"}, + {1157, L"ERROR_DLL_NOT_FOUND"}, + {1158, L"ERROR_NO_MORE_USER_HANDLES"}, + {1159, L"ERROR_MESSAGE_SYNC_ONLY"}, + {1160, L"ERROR_SOURCE_ELEMENT_EMPTY"}, + {1161, L"ERROR_DESTINATION_ELEMENT_FULL"}, + {1162, L"ERROR_ILLEGAL_ELEMENT_ADDRESS"}, + {1163, L"ERROR_MAGAZINE_NOT_PRESENT"}, + {1164, L"ERROR_DEVICE_REINITIALIZATION_NEEDED"}, + {1165, L"ERROR_DEVICE_REQUIRES_CLEANING"}, + {1166, L"ERROR_DEVICE_DOOR_OPEN"}, + {1167, L"ERROR_DEVICE_NOT_CONNECTED"}, + {1168, L"ERROR_NOT_FOUND"}, + {1169, L"ERROR_NO_MATCH"}, + {1170, L"ERROR_SET_NOT_FOUND"}, + {1171, L"ERROR_POINT_NOT_FOUND"}, + {1172, L"ERROR_NO_TRACKING_SERVICE"}, + {1173, L"ERROR_NO_VOLUME_ID"}, + {1175, L"ERROR_UNABLE_TO_REMOVE_REPLACED"}, + {1176, L"ERROR_UNABLE_TO_MOVE_REPLACEMENT"}, + {1177, L"ERROR_UNABLE_TO_MOVE_REPLACEMENT_2"}, + {1178, L"ERROR_JOURNAL_DELETE_IN_PROGRESS"}, + {1179, L"ERROR_JOURNAL_NOT_ACTIVE"}, + {1180, L"ERROR_POTENTIAL_FILE_FOUND"}, + {1181, L"ERROR_JOURNAL_ENTRY_DELETED"}, + {1190, L"ERROR_SHUTDOWN_IS_SCHEDULED"}, + {1191, L"ERROR_SHUTDOWN_USERS_LOGGED_ON"}, + {1200, L"ERROR_BAD_DEVICE"}, + {1201, L"ERROR_CONNECTION_UNAVAIL"}, + {1202, L"ERROR_DEVICE_ALREADY_REMEMBERED"}, + {1203, L"ERROR_NO_NET_OR_BAD_PATH"}, + {1204, L"ERROR_BAD_PROVIDER"}, + {1205, L"ERROR_CANNOT_OPEN_PROFILE"}, + {1206, L"ERROR_BAD_PROFILE"}, + {1207, L"ERROR_NOT_CONTAINER"}, + {1208, L"ERROR_EXTENDED_ERROR"}, + {1209, L"ERROR_INVALID_GROUPNAME"}, + {1210, L"ERROR_INVALID_COMPUTERNAME"}, + {1211, L"ERROR_INVALID_EVENTNAME"}, + {1212, L"ERROR_INVALID_DOMAINNAME"}, + {1213, L"ERROR_INVALID_SERVICENAME"}, + {1214, L"ERROR_INVALID_NETNAME"}, + {1215, L"ERROR_INVALID_SHARENAME"}, + {1216, L"ERROR_INVALID_PASSWORDNAME"}, + {1217, L"ERROR_INVALID_MESSAGENAME"}, + {1218, L"ERROR_INVALID_MESSAGEDEST"}, + {1219, L"ERROR_SESSION_CREDENTIAL_CONFLICT"}, + {1220, L"ERROR_REMOTE_SESSION_LIMIT_EXCEEDED"}, + {1221, L"ERROR_DUP_DOMAINNAME"}, + {1222, L"ERROR_NO_NETWORK"}, + {1223, L"ERROR_CANCELLED"}, + {1224, L"ERROR_USER_MAPPED_FILE"}, + {1225, L"ERROR_CONNECTION_REFUSED"}, + {1226, L"ERROR_GRACEFUL_DISCONNECT"}, + {1227, L"ERROR_ADDRESS_ALREADY_ASSOCIATED"}, + {1228, L"ERROR_ADDRESS_NOT_ASSOCIATED"}, + {1229, L"ERROR_CONNECTION_INVALID"}, + {1230, L"ERROR_CONNECTION_ACTIVE"}, + {1231, L"ERROR_NETWORK_UNREACHABLE"}, + {1232, L"ERROR_HOST_UNREACHABLE"}, + {1233, L"ERROR_PROTOCOL_UNREACHABLE"}, + {1234, L"ERROR_PORT_UNREACHABLE"}, + {1235, L"ERROR_REQUEST_ABORTED"}, + {1236, L"ERROR_CONNECTION_ABORTED"}, + {1237, L"ERROR_RETRY"}, + {1238, L"ERROR_CONNECTION_COUNT_LIMIT"}, + {1239, L"ERROR_LOGIN_TIME_RESTRICTION"}, + {1240, L"ERROR_LOGIN_WKSTA_RESTRICTION"}, + {1241, L"ERROR_INCORRECT_ADDRESS"}, + {1242, L"ERROR_ALREADY_REGISTERED"}, + {1243, L"ERROR_SERVICE_NOT_FOUND"}, + {1244, L"ERROR_NOT_AUTHENTICATED"}, + {1245, L"ERROR_NOT_LOGGED_ON"}, + {1246, L"ERROR_CONTINUE"}, + {1247, L"ERROR_ALREADY_INITIALIZED"}, + {1248, L"ERROR_NO_MORE_DEVICES"}, + {1249, L"ERROR_NO_SUCH_SITE"}, + {1250, L"ERROR_DOMAIN_CONTROLLER_EXISTS"}, + {1251, L"ERROR_ONLY_IF_CONNECTED"}, + {1252, L"ERROR_OVERRIDE_NOCHANGES"}, + {1253, L"ERROR_BAD_USER_PROFILE"}, + {1254, L"ERROR_NOT_SUPPORTED_ON_SBS"}, + {1255, L"ERROR_SERVER_SHUTDOWN_IN_PROGRESS"}, + {1256, L"ERROR_HOST_DOWN"}, + {1257, L"ERROR_NON_ACCOUNT_SID"}, + {1258, L"ERROR_NON_DOMAIN_SID"}, + {1259, L"ERROR_APPHELP_BLOCK"}, + {1260, L"ERROR_ACCESS_DISABLED_BY_POLICY"}, + {1261, L"ERROR_REG_NAT_CONSUMPTION"}, + {1262, L"ERROR_CSCSHARE_OFFLINE"}, + {1263, L"ERROR_PKINIT_FAILURE"}, + {1264, L"ERROR_SMARTCARD_SUBSYSTEM_FAILURE"}, + {1265, L"ERROR_DOWNGRADE_DETECTED"}, + {1271, L"ERROR_MACHINE_LOCKED"}, + {1273, L"ERROR_CALLBACK_SUPPLIED_INVALID_DATA"}, + {1274, L"ERROR_SYNC_FOREGROUND_REFRESH_REQUIRED"}, + {1275, L"ERROR_DRIVER_BLOCKED"}, + {1276, L"ERROR_INVALID_IMPORT_OF_NON_DLL"}, + {1277, L"ERROR_ACCESS_DISABLED_WEBBLADE"}, + {1278, L"ERROR_ACCESS_DISABLED_WEBBLADE_TAMPER"}, + {1279, L"ERROR_RECOVERY_FAILURE"}, + {1280, L"ERROR_ALREADY_FIBER"}, + {1281, L"ERROR_ALREADY_THREAD"}, + {1282, L"ERROR_STACK_BUFFER_OVERRUN"}, + {1283, L"ERROR_PARAMETER_QUOTA_EXCEEDED"}, + {1284, L"ERROR_DEBUGGER_INACTIVE"}, + {1285, L"ERROR_DELAY_LOAD_FAILED"}, + {1286, L"ERROR_VDM_DISALLOWED"}, + {1287, L"ERROR_UNIDENTIFIED_ERROR"}, + {1288, L"ERROR_INVALID_CRUNTIME_PARAMETER"}, + {1289, L"ERROR_BEYOND_VDL"}, + {1290, L"ERROR_INCOMPATIBLE_SERVICE_SID_TYPE"}, + {1291, L"ERROR_DRIVER_PROCESS_TERMINATED"}, + {1292, L"ERROR_IMPLEMENTATION_LIMIT"}, + {1293, L"ERROR_PROCESS_IS_PROTECTED"}, + {1294, L"ERROR_SERVICE_NOTIFY_CLIENT_LAGGING"}, + {1295, L"ERROR_DISK_QUOTA_EXCEEDED"}, + {1296, L"ERROR_CONTENT_BLOCKED"}, + {1297, L"ERROR_INCOMPATIBLE_SERVICE_PRIVILEGE"}, + {1298, L"ERROR_APP_HANG"}, + {1299, L"ERROR_INVALID_LABEL"}, + {1300, L"ERROR_NOT_ALL_ASSIGNED"}, + {1301, L"ERROR_SOME_NOT_MAPPED"}, + {1302, L"ERROR_NO_QUOTAS_FOR_ACCOUNT"}, + {1303, L"ERROR_LOCAL_USER_SESSION_KEY"}, + {1304, L"ERROR_NULL_LM_PASSWORD"}, + {1305, L"ERROR_UNKNOWN_REVISION"}, + {1306, L"ERROR_REVISION_MISMATCH"}, + {1307, L"ERROR_INVALID_OWNER"}, + {1308, L"ERROR_INVALID_PRIMARY_GROUP"}, + {1309, L"ERROR_NO_IMPERSONATION_TOKEN"}, + {1310, L"ERROR_CANT_DISABLE_MANDATORY"}, + {1311, L"ERROR_NO_LOGON_SERVERS"}, + {1312, L"ERROR_NO_SUCH_LOGON_SESSION"}, + {1313, L"ERROR_NO_SUCH_PRIVILEGE"}, + {1314, L"ERROR_PRIVILEGE_NOT_HELD"}, + {1315, L"ERROR_INVALID_ACCOUNT_NAME"}, + {1316, L"ERROR_USER_EXISTS"}, + {1317, L"ERROR_NO_SUCH_USER"}, + {1318, L"ERROR_GROUP_EXISTS"}, + {1319, L"ERROR_NO_SUCH_GROUP"}, + {1320, L"ERROR_MEMBER_IN_GROUP"}, + {1321, L"ERROR_MEMBER_NOT_IN_GROUP"}, + {1322, L"ERROR_LAST_ADMIN"}, + {1323, L"ERROR_WRONG_PASSWORD"}, + {1324, L"ERROR_ILL_FORMED_PASSWORD"}, + {1325, L"ERROR_PASSWORD_RESTRICTION"}, + {1326, L"ERROR_LOGON_FAILURE"}, + {1327, L"ERROR_ACCOUNT_RESTRICTION"}, + {1328, L"ERROR_INVALID_LOGON_HOURS"}, + {1329, L"ERROR_INVALID_WORKSTATION"}, + {1330, L"ERROR_PASSWORD_EXPIRED"}, + {1331, L"ERROR_ACCOUNT_DISABLED"}, + {1332, L"ERROR_NONE_MAPPED"}, + {1333, L"ERROR_TOO_MANY_LUIDS_REQUESTED"}, + {1334, L"ERROR_LUIDS_EXHAUSTED"}, + {1335, L"ERROR_INVALID_SUB_AUTHORITY"}, + {1336, L"ERROR_INVALID_ACL"}, + {1337, L"ERROR_INVALID_SID"}, + {1338, L"ERROR_INVALID_SECURITY_DESCR"}, + {1340, L"ERROR_BAD_INHERITANCE_ACL"}, + {1341, L"ERROR_SERVER_DISABLED"}, + {1342, L"ERROR_SERVER_NOT_DISABLED"}, + {1343, L"ERROR_INVALID_ID_AUTHORITY"}, + {1344, L"ERROR_ALLOTTED_SPACE_EXCEEDED"}, + {1345, L"ERROR_INVALID_GROUP_ATTRIBUTES"}, + {1346, L"ERROR_BAD_IMPERSONATION_LEVEL"}, + {1347, L"ERROR_CANT_OPEN_ANONYMOUS"}, + {1348, L"ERROR_BAD_VALIDATION_CLASS"}, + {1349, L"ERROR_BAD_TOKEN_TYPE"}, + {1350, L"ERROR_NO_SECURITY_ON_OBJECT"}, + {1351, L"ERROR_CANT_ACCESS_DOMAIN_INFO"}, + {1352, L"ERROR_INVALID_SERVER_STATE"}, + {1353, L"ERROR_INVALID_DOMAIN_STATE"}, + {1354, L"ERROR_INVALID_DOMAIN_ROLE"}, + {1355, L"ERROR_NO_SUCH_DOMAIN"}, + {1356, L"ERROR_DOMAIN_EXISTS"}, + {1357, L"ERROR_DOMAIN_LIMIT_EXCEEDED"}, + {1358, L"ERROR_INTERNAL_DB_CORRUPTION"}, + {1359, L"ERROR_INTERNAL_ERROR"}, + {1360, L"ERROR_GENERIC_NOT_MAPPED"}, + {1361, L"ERROR_BAD_DESCRIPTOR_FORMAT"}, + {1362, L"ERROR_NOT_LOGON_PROCESS"}, + {1363, L"ERROR_LOGON_SESSION_EXISTS"}, + {1364, L"ERROR_NO_SUCH_PACKAGE"}, + {1365, L"ERROR_BAD_LOGON_SESSION_STATE"}, + {1366, L"ERROR_LOGON_SESSION_COLLISION"}, + {1367, L"ERROR_INVALID_LOGON_TYPE"}, + {1368, L"ERROR_CANNOT_IMPERSONATE"}, + {1369, L"ERROR_RXACT_INVALID_STATE"}, + {1370, L"ERROR_RXACT_COMMIT_FAILURE"}, + {1371, L"ERROR_SPECIAL_ACCOUNT"}, + {1372, L"ERROR_SPECIAL_GROUP"}, + {1373, L"ERROR_SPECIAL_USER"}, + {1374, L"ERROR_MEMBERS_PRIMARY_GROUP"}, + {1375, L"ERROR_TOKEN_ALREADY_IN_USE"}, + {1376, L"ERROR_NO_SUCH_ALIAS"}, + {1377, L"ERROR_MEMBER_NOT_IN_ALIAS"}, + {1378, L"ERROR_MEMBER_IN_ALIAS"}, + {1379, L"ERROR_ALIAS_EXISTS"}, + {1380, L"ERROR_LOGON_NOT_GRANTED"}, + {1381, L"ERROR_TOO_MANY_SECRETS"}, + {1382, L"ERROR_SECRET_TOO_LONG"}, + {1383, L"ERROR_INTERNAL_DB_ERROR"}, + {1384, L"ERROR_TOO_MANY_CONTEXT_IDS"}, + {1385, L"ERROR_LOGON_TYPE_NOT_GRANTED"}, + {1386, L"ERROR_NT_CROSS_ENCRYPTION_REQUIRED"}, + {1387, L"ERROR_NO_SUCH_MEMBER"}, + {1388, L"ERROR_INVALID_MEMBER"}, + {1389, L"ERROR_TOO_MANY_SIDS"}, + {1390, L"ERROR_LM_CROSS_ENCRYPTION_REQUIRED"}, + {1391, L"ERROR_NO_INHERITANCE"}, + {1392, L"ERROR_FILE_CORRUPT"}, + {1393, L"ERROR_DISK_CORRUPT"}, + {1394, L"ERROR_NO_USER_SESSION_KEY"}, + {1395, L"ERROR_LICENSE_QUOTA_EXCEEDED"}, + {1396, L"ERROR_WRONG_TARGET_NAME"}, + {1397, L"ERROR_MUTUAL_AUTH_FAILED"}, + {1398, L"ERROR_TIME_SKEW"}, + {1399, L"ERROR_CURRENT_DOMAIN_NOT_ALLOWED"}, + {1400, L"ERROR_INVALID_WINDOW_HANDLE"}, + {1401, L"ERROR_INVALID_MENU_HANDLE"}, + {1402, L"ERROR_INVALID_CURSOR_HANDLE"}, + {1403, L"ERROR_INVALID_ACCEL_HANDLE"}, + {1404, L"ERROR_INVALID_HOOK_HANDLE"}, + {1405, L"ERROR_INVALID_DWP_HANDLE"}, + {1406, L"ERROR_TLW_WITH_WSCHILD"}, + {1407, L"ERROR_CANNOT_FIND_WND_CLASS"}, + {1408, L"ERROR_WINDOW_OF_OTHER_THREAD"}, + {1409, L"ERROR_HOTKEY_ALREADY_REGISTERED"}, + {1410, L"ERROR_CLASS_ALREADY_EXISTS"}, + {1411, L"ERROR_CLASS_DOES_NOT_EXIST"}, + {1412, L"ERROR_CLASS_HAS_WINDOWS"}, + {1413, L"ERROR_INVALID_INDEX"}, + {1414, L"ERROR_INVALID_ICON_HANDLE"}, + {1415, L"ERROR_PRIVATE_DIALOG_INDEX"}, + {1416, L"ERROR_LISTBOX_ID_NOT_FOUND"}, + {1417, L"ERROR_NO_WILDCARD_CHARACTERS"}, + {1418, L"ERROR_CLIPBOARD_NOT_OPEN"}, + {1419, L"ERROR_HOTKEY_NOT_REGISTERED"}, + {1420, L"ERROR_WINDOW_NOT_DIALOG"}, + {1421, L"ERROR_CONTROL_ID_NOT_FOUND"}, + {1422, L"ERROR_INVALID_COMBOBOX_MESSAGE"}, + {1423, L"ERROR_WINDOW_NOT_COMBOBOX"}, + {1424, L"ERROR_INVALID_EDIT_HEIGHT"}, + {1425, L"ERROR_DC_NOT_FOUND"}, + {1426, L"ERROR_INVALID_HOOK_FILTER"}, + {1427, L"ERROR_INVALID_FILTER_PROC"}, + {1428, L"ERROR_HOOK_NEEDS_HMOD"}, + {1429, L"ERROR_GLOBAL_ONLY_HOOK"}, + {1430, L"ERROR_JOURNAL_HOOK_SET"}, + {1431, L"ERROR_HOOK_NOT_INSTALLED"}, + {1432, L"ERROR_INVALID_LB_MESSAGE"}, + {1433, L"ERROR_SETCOUNT_ON_BAD_LB"}, + {1434, L"ERROR_LB_WITHOUT_TABSTOPS"}, + {1435, L"ERROR_DESTROY_OBJECT_OF_OTHER_THREAD"}, + {1436, L"ERROR_CHILD_WINDOW_MENU"}, + {1437, L"ERROR_NO_SYSTEM_MENU"}, + {1438, L"ERROR_INVALID_MSGBOX_STYLE"}, + {1439, L"ERROR_INVALID_SPI_VALUE"}, + {1440, L"ERROR_SCREEN_ALREADY_LOCKED"}, + {1441, L"ERROR_HWNDS_HAVE_DIFF_PARENT"}, + {1442, L"ERROR_NOT_CHILD_WINDOW"}, + {1443, L"ERROR_INVALID_GW_COMMAND"}, + {1444, L"ERROR_INVALID_THREAD_ID"}, + {1445, L"ERROR_NON_MDICHILD_WINDOW"}, + {1446, L"ERROR_POPUP_ALREADY_ACTIVE"}, + {1447, L"ERROR_NO_SCROLLBARS"}, + {1448, L"ERROR_INVALID_SCROLLBAR_RANGE"}, + {1449, L"ERROR_INVALID_SHOWWIN_COMMAND"}, + {1450, L"ERROR_NO_SYSTEM_RESOURCES"}, + {1451, L"ERROR_NONPAGED_SYSTEM_RESOURCES"}, + {1452, L"ERROR_PAGED_SYSTEM_RESOURCES"}, + {1453, L"ERROR_WORKING_SET_QUOTA"}, + {1454, L"ERROR_PAGEFILE_QUOTA"}, + {1455, L"ERROR_COMMITMENT_LIMIT"}, + {1456, L"ERROR_MENU_ITEM_NOT_FOUND"}, + {1457, L"ERROR_INVALID_KEYBOARD_HANDLE"}, + {1458, L"ERROR_HOOK_TYPE_NOT_ALLOWED"}, + {1459, L"ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION"}, + {1460, L"ERROR_TIMEOUT"}, + {1461, L"ERROR_INVALID_MONITOR_HANDLE"}, + {1462, L"ERROR_INCORRECT_SIZE"}, + {1463, L"ERROR_SYMLINK_CLASS_DISABLED"}, + {1464, L"ERROR_SYMLINK_NOT_SUPPORTED"}, + {1465, L"ERROR_XML_PARSE_ERROR"}, + {1466, L"ERROR_XMLDSIG_ERROR"}, + {1467, L"ERROR_RESTART_APPLICATION"}, + {1468, L"ERROR_WRONG_COMPARTMENT"}, + {1469, L"ERROR_AUTHIP_FAILURE"}, + {1470, L"ERROR_NO_NVRAM_RESOURCES"}, + {1471, L"ERROR_NOT_GUI_PROCESS"}, + {1500, L"ERROR_EVENTLOG_FILE_CORRUPT"}, + {1501, L"ERROR_EVENTLOG_CANT_START"}, + {1502, L"ERROR_LOG_FILE_FULL"}, + {1503, L"ERROR_EVENTLOG_FILE_CHANGED"}, + {1550, L"ERROR_INVALID_TASK_NAME"}, + {1551, L"ERROR_INVALID_TASK_INDEX"}, + {1552, L"ERROR_THREAD_ALREADY_IN_TASK"}, + {1601, L"ERROR_INSTALL_SERVICE_FAILURE"}, + {1602, L"ERROR_INSTALL_USEREXIT"}, + {1603, L"ERROR_INSTALL_FAILURE"}, + {1604, L"ERROR_INSTALL_SUSPEND"}, + {1605, L"ERROR_UNKNOWN_PRODUCT"}, + {1606, L"ERROR_UNKNOWN_FEATURE"}, + {1607, L"ERROR_UNKNOWN_COMPONENT"}, + {1608, L"ERROR_UNKNOWN_PROPERTY"}, + {1609, L"ERROR_INVALID_HANDLE_STATE"}, + {1610, L"ERROR_BAD_CONFIGURATION"}, + {1611, L"ERROR_INDEX_ABSENT"}, + {1612, L"ERROR_INSTALL_SOURCE_ABSENT"}, + {1613, L"ERROR_INSTALL_PACKAGE_VERSION"}, + {1614, L"ERROR_PRODUCT_UNINSTALLED"}, + {1615, L"ERROR_BAD_QUERY_SYNTAX"}, + {1616, L"ERROR_INVALID_FIELD"}, + {1617, L"ERROR_DEVICE_REMOVED"}, + {1618, L"ERROR_INSTALL_ALREADY_RUNNING"}, + {1619, L"ERROR_INSTALL_PACKAGE_OPEN_FAILED"}, + {1620, L"ERROR_INSTALL_PACKAGE_INVALID"}, + {1621, L"ERROR_INSTALL_UI_FAILURE"}, + {1622, L"ERROR_INSTALL_LOG_FAILURE"}, + {1623, L"ERROR_INSTALL_LANGUAGE_UNSUPPORTED"}, + {1624, L"ERROR_INSTALL_TRANSFORM_FAILURE"}, + {1625, L"ERROR_INSTALL_PACKAGE_REJECTED"}, + {1626, L"ERROR_FUNCTION_NOT_CALLED"}, + {1627, L"ERROR_FUNCTION_FAILED"}, + {1628, L"ERROR_INVALID_TABLE"}, + {1629, L"ERROR_DATATYPE_MISMATCH"}, + {1630, L"ERROR_UNSUPPORTED_TYPE"}, + {1631, L"ERROR_CREATE_FAILED"}, + {1632, L"ERROR_INSTALL_TEMP_UNWRITABLE"}, + {1633, L"ERROR_INSTALL_PLATFORM_UNSUPPORTED"}, + {1634, L"ERROR_INSTALL_NOTUSED"}, + {1635, L"ERROR_PATCH_PACKAGE_OPEN_FAILED"}, + {1636, L"ERROR_PATCH_PACKAGE_INVALID"}, + {1637, L"ERROR_PATCH_PACKAGE_UNSUPPORTED"}, + {1638, L"ERROR_PRODUCT_VERSION"}, + {1639, L"ERROR_INVALID_COMMAND_LINE"}, + {1640, L"ERROR_INSTALL_REMOTE_DISALLOWED"}, + {1641, L"ERROR_SUCCESS_REBOOT_INITIATED"}, + {1642, L"ERROR_PATCH_TARGET_NOT_FOUND"}, + {1643, L"ERROR_PATCH_PACKAGE_REJECTED"}, + {1644, L"ERROR_INSTALL_TRANSFORM_REJECTED"}, + {1645, L"ERROR_INSTALL_REMOTE_PROHIBITED"}, + {1646, L"ERROR_PATCH_REMOVAL_UNSUPPORTED"}, + {1647, L"ERROR_UNKNOWN_PATCH"}, + {1648, L"ERROR_PATCH_NO_SEQUENCE"}, + {1649, L"ERROR_PATCH_REMOVAL_DISALLOWED"}, + {1650, L"ERROR_INVALID_PATCH_XML"}, + {1651, L"ERROR_PATCH_MANAGED_ADVERTISED_PRODUCT"}, + {1652, L"ERROR_INSTALL_SERVICE_SAFEBOOT"}, + {1653, L"ERROR_FAIL_FAST_EXCEPTION"}, + {1654, L"ERROR_INSTALL_REJECTED"}, + {1700, L"RPC_S_INVALID_STRING_BINDING"}, + {1701, L"RPC_S_WRONG_KIND_OF_BINDING"}, + {1702, L"RPC_S_INVALID_BINDING"}, + {1703, L"RPC_S_PROTSEQ_NOT_SUPPORTED"}, + {1704, L"RPC_S_INVALID_RPC_PROTSEQ"}, + {1705, L"RPC_S_INVALID_STRING_UUID"}, + {1706, L"RPC_S_INVALID_ENDPOINT_FORMAT"}, + {1707, L"RPC_S_INVALID_NET_ADDR"}, + {1708, L"RPC_S_NO_ENDPOINT_FOUND"}, + {1709, L"RPC_S_INVALID_TIMEOUT"}, + {1710, L"RPC_S_OBJECT_NOT_FOUND"}, + {1711, L"RPC_S_ALREADY_REGISTERED"}, + {1712, L"RPC_S_TYPE_ALREADY_REGISTERED"}, + {1713, L"RPC_S_ALREADY_LISTENING"}, + {1714, L"RPC_S_NO_PROTSEQS_REGISTERED"}, + {1715, L"RPC_S_NOT_LISTENING"}, + {1716, L"RPC_S_UNKNOWN_MGR_TYPE"}, + {1717, L"RPC_S_UNKNOWN_IF"}, + {1718, L"RPC_S_NO_BINDINGS"}, + {1719, L"RPC_S_NO_PROTSEQS"}, + {1720, L"RPC_S_CANT_CREATE_ENDPOINT"}, + {1721, L"RPC_S_OUT_OF_RESOURCES"}, + {1722, L"RPC_S_SERVER_UNAVAILABLE"}, + {1723, L"RPC_S_SERVER_TOO_BUSY"}, + {1724, L"RPC_S_INVALID_NETWORK_OPTIONS"}, + {1725, L"RPC_S_NO_CALL_ACTIVE"}, + {1726, L"RPC_S_CALL_FAILED"}, + {1727, L"RPC_S_CALL_FAILED_DNE"}, + {1728, L"RPC_S_PROTOCOL_ERROR"}, + {1729, L"RPC_S_PROXY_ACCESS_DENIED"}, + {1730, L"RPC_S_UNSUPPORTED_TRANS_SYN"}, + {1732, L"RPC_S_UNSUPPORTED_TYPE"}, + {1733, L"RPC_S_INVALID_TAG"}, + {1734, L"RPC_S_INVALID_BOUND"}, + {1735, L"RPC_S_NO_ENTRY_NAME"}, + {1736, L"RPC_S_INVALID_NAME_SYNTAX"}, + {1737, L"RPC_S_UNSUPPORTED_NAME_SYNTAX"}, + {1739, L"RPC_S_UUID_NO_ADDRESS"}, + {1740, L"RPC_S_DUPLICATE_ENDPOINT"}, + {1741, L"RPC_S_UNKNOWN_AUTHN_TYPE"}, + {1742, L"RPC_S_MAX_CALLS_TOO_SMALL"}, + {1743, L"RPC_S_STRING_TOO_LONG"}, + {1744, L"RPC_S_PROTSEQ_NOT_FOUND"}, + {1745, L"RPC_S_PROCNUM_OUT_OF_RANGE"}, + {1746, L"RPC_S_BINDING_HAS_NO_AUTH"}, + {1747, L"RPC_S_UNKNOWN_AUTHN_SERVICE"}, + {1748, L"RPC_S_UNKNOWN_AUTHN_LEVEL"}, + {1749, L"RPC_S_INVALID_AUTH_IDENTITY"}, + {1750, L"RPC_S_UNKNOWN_AUTHZ_SERVICE"}, + {1751, L"EPT_S_INVALID_ENTRY"}, + {1752, L"EPT_S_CANT_PERFORM_OP"}, + {1753, L"EPT_S_NOT_REGISTERED"}, + {1754, L"RPC_S_NOTHING_TO_EXPORT"}, + {1755, L"RPC_S_INCOMPLETE_NAME"}, + {1756, L"RPC_S_INVALID_VERS_OPTION"}, + {1757, L"RPC_S_NO_MORE_MEMBERS"}, + {1758, L"RPC_S_NOT_ALL_OBJS_UNEXPORTED"}, + {1759, L"RPC_S_INTERFACE_NOT_FOUND"}, + {1760, L"RPC_S_ENTRY_ALREADY_EXISTS"}, + {1761, L"RPC_S_ENTRY_NOT_FOUND"}, + {1762, L"RPC_S_NAME_SERVICE_UNAVAILABLE"}, + {1763, L"RPC_S_INVALID_NAF_ID"}, + {1764, L"RPC_S_CANNOT_SUPPORT"}, + {1765, L"RPC_S_NO_CONTEXT_AVAILABLE"}, + {1766, L"RPC_S_INTERNAL_ERROR"}, + {1767, L"RPC_S_ZERO_DIVIDE"}, + {1768, L"RPC_S_ADDRESS_ERROR"}, + {1769, L"RPC_S_FP_DIV_ZERO"}, + {1770, L"RPC_S_FP_UNDERFLOW"}, + {1771, L"RPC_S_FP_OVERFLOW"}, + {1772, L"RPC_X_NO_MORE_ENTRIES"}, + {1773, L"RPC_X_SS_CHAR_TRANS_OPEN_FAIL"}, + {1774, L"RPC_X_SS_CHAR_TRANS_SHORT_FILE"}, + {1775, L"RPC_X_SS_IN_NULL_CONTEXT"}, + {1777, L"RPC_X_SS_CONTEXT_DAMAGED"}, + {1778, L"RPC_X_SS_HANDLES_MISMATCH"}, + {1779, L"RPC_X_SS_CANNOT_GET_CALL_HANDLE"}, + {1780, L"RPC_X_NULL_REF_POINTER"}, + {1781, L"RPC_X_ENUM_VALUE_OUT_OF_RANGE"}, + {1782, L"RPC_X_BYTE_COUNT_TOO_SMALL"}, + {1783, L"RPC_X_BAD_STUB_DATA"}, + {1784, L"ERROR_INVALID_USER_BUFFER"}, + {1785, L"ERROR_UNRECOGNIZED_MEDIA"}, + {1786, L"ERROR_NO_TRUST_LSA_SECRET"}, + {1787, L"ERROR_NO_TRUST_SAM_ACCOUNT"}, + {1788, L"ERROR_TRUSTED_DOMAIN_FAILURE"}, + {1789, L"ERROR_TRUSTED_RELATIONSHIP_FAILURE"}, + {1790, L"ERROR_TRUST_FAILURE"}, + {1791, L"RPC_S_CALL_IN_PROGRESS"}, + {1792, L"ERROR_NETLOGON_NOT_STARTED"}, + {1793, L"ERROR_ACCOUNT_EXPIRED"}, + {1794, L"ERROR_REDIRECTOR_HAS_OPEN_HANDLES"}, + {1795, L"ERROR_PRINTER_DRIVER_ALREADY_INSTALLED"}, + {1796, L"ERROR_UNKNOWN_PORT"}, + {1797, L"ERROR_UNKNOWN_PRINTER_DRIVER"}, + {1798, L"ERROR_UNKNOWN_PRINTPROCESSOR"}, + {1799, L"ERROR_INVALID_SEPARATOR_FILE"}, + {1800, L"ERROR_INVALID_PRIORITY"}, + {1801, L"ERROR_INVALID_PRINTER_NAME"}, + {1802, L"ERROR_PRINTER_ALREADY_EXISTS"}, + {1803, L"ERROR_INVALID_PRINTER_COMMAND"}, + {1804, L"ERROR_INVALID_DATATYPE"}, + {1805, L"ERROR_INVALID_ENVIRONMENT"}, + {1806, L"RPC_S_NO_MORE_BINDINGS"}, + {1807, L"ERROR_NOLOGON_INTERDOMAIN_TRUST_ACCOUNT"}, + {1808, L"ERROR_NOLOGON_WORKSTATION_TRUST_ACCOUNT"}, + {1809, L"ERROR_NOLOGON_SERVER_TRUST_ACCOUNT"}, + {1810, L"ERROR_DOMAIN_TRUST_INCONSISTENT"}, + {1811, L"ERROR_SERVER_HAS_OPEN_HANDLES"}, + {1812, L"ERROR_RESOURCE_DATA_NOT_FOUND"}, + {1813, L"ERROR_RESOURCE_TYPE_NOT_FOUND"}, + {1814, L"ERROR_RESOURCE_NAME_NOT_FOUND"}, + {1815, L"ERROR_RESOURCE_LANG_NOT_FOUND"}, + {1816, L"ERROR_NOT_ENOUGH_QUOTA"}, + {1817, L"RPC_S_NO_INTERFACES"}, + {1818, L"RPC_S_CALL_CANCELLED"}, + {1819, L"RPC_S_BINDING_INCOMPLETE"}, + {1820, L"RPC_S_COMM_FAILURE"}, + {1821, L"RPC_S_UNSUPPORTED_AUTHN_LEVEL"}, + {1822, L"RPC_S_NO_PRINC_NAME"}, + {1823, L"RPC_S_NOT_RPC_ERROR"}, + {1824, L"RPC_S_UUID_LOCAL_ONLY"}, + {1825, L"RPC_S_SEC_PKG_ERROR"}, + {1826, L"RPC_S_NOT_CANCELLED"}, + {1827, L"RPC_X_INVALID_ES_ACTION"}, + {1828, L"RPC_X_WRONG_ES_VERSION"}, + {1829, L"RPC_X_WRONG_STUB_VERSION"}, + {1830, L"RPC_X_INVALID_PIPE_OBJECT"}, + {1831, L"RPC_X_WRONG_PIPE_ORDER"}, + {1832, L"RPC_X_WRONG_PIPE_VERSION"}, + {1833, L"RPC_S_COOKIE_AUTH_FAILED"}, + {1898, L"RPC_S_GROUP_MEMBER_NOT_FOUND"}, + {1899, L"EPT_S_CANT_CREATE"}, + {1900, L"RPC_S_INVALID_OBJECT"}, + {1901, L"ERROR_INVALID_TIME"}, + {1902, L"ERROR_INVALID_FORM_NAME"}, + {1903, L"ERROR_INVALID_FORM_SIZE"}, + {1904, L"ERROR_ALREADY_WAITING"}, + {1905, L"ERROR_PRINTER_DELETED"}, + {1906, L"ERROR_INVALID_PRINTER_STATE"}, + {1907, L"ERROR_PASSWORD_MUST_CHANGE"}, + {1908, L"ERROR_DOMAIN_CONTROLLER_NOT_FOUND"}, + {1909, L"ERROR_ACCOUNT_LOCKED_OUT"}, + {1910, L"OR_INVALID_OXID"}, + {1911, L"OR_INVALID_OID"}, + {1912, L"OR_INVALID_SET"}, + {1913, L"RPC_S_SEND_INCOMPLETE"}, + {1914, L"RPC_S_INVALID_ASYNC_HANDLE"}, + {1915, L"RPC_S_INVALID_ASYNC_CALL"}, + {1916, L"RPC_X_PIPE_CLOSED"}, + {1917, L"RPC_X_PIPE_DISCIPLINE_ERROR"}, + {1918, L"RPC_X_PIPE_EMPTY"}, + {1919, L"ERROR_NO_SITENAME"}, + {1920, L"ERROR_CANT_ACCESS_FILE"}, + {1921, L"ERROR_CANT_RESOLVE_FILENAME"}, + {1922, L"RPC_S_ENTRY_TYPE_MISMATCH"}, + {1923, L"RPC_S_NOT_ALL_OBJS_EXPORTED"}, + {1924, L"RPC_S_INTERFACE_NOT_EXPORTED"}, + {1925, L"RPC_S_PROFILE_NOT_ADDED"}, + {1926, L"RPC_S_PRF_ELT_NOT_ADDED"}, + {1927, L"RPC_S_PRF_ELT_NOT_REMOVED"}, + {1928, L"RPC_S_GRP_ELT_NOT_ADDED"}, + {1929, L"RPC_S_GRP_ELT_NOT_REMOVED"}, + {1930, L"ERROR_KM_DRIVER_BLOCKED"}, + {1931, L"ERROR_CONTEXT_EXPIRED"}, + {1932, L"ERROR_PER_USER_TRUST_QUOTA_EXCEEDED"}, + {1933, L"ERROR_ALL_USER_TRUST_QUOTA_EXCEEDED"}, + {1934, L"ERROR_USER_DELETE_TRUST_QUOTA_EXCEEDED"}, + {1935, L"ERROR_AUTHENTICATION_FIREWALL_FAILED"}, + {1936, L"ERROR_REMOTE_PRINT_CONNECTIONS_BLOCKED"}, + {1937, L"ERROR_NTLM_BLOCKED"}, + {1938, L"ERROR_PASSWORD_CHANGE_REQUIRED"}, + {2000, L"ERROR_INVALID_PIXEL_FORMAT"}, + {2001, L"ERROR_BAD_DRIVER"}, + {2002, L"ERROR_INVALID_WINDOW_STYLE"}, + {2003, L"ERROR_METAFILE_NOT_SUPPORTED"}, + {2004, L"ERROR_TRANSFORM_NOT_SUPPORTED"}, + {2005, L"ERROR_CLIPPING_NOT_SUPPORTED"}, + {2010, L"ERROR_INVALID_CMM"}, + {2011, L"ERROR_INVALID_PROFILE"}, + {2012, L"ERROR_TAG_NOT_FOUND"}, + {2013, L"ERROR_TAG_NOT_PRESENT"}, + {2014, L"ERROR_DUPLICATE_TAG"}, + {2015, L"ERROR_PROFILE_NOT_ASSOCIATED_WITH_DEVICE"}, + {2016, L"ERROR_PROFILE_NOT_FOUND"}, + {2017, L"ERROR_INVALID_COLORSPACE"}, + {2018, L"ERROR_ICM_NOT_ENABLED"}, + {2019, L"ERROR_DELETING_ICM_XFORM"}, + {2020, L"ERROR_INVALID_TRANSFORM"}, + {2021, L"ERROR_COLORSPACE_MISMATCH"}, + {2022, L"ERROR_INVALID_COLORINDEX"}, + {2023, L"ERROR_PROFILE_DOES_NOT_MATCH_DEVICE"}, + {2108, L"ERROR_CONNECTED_OTHER_PASSWORD"}, + {2109, L"ERROR_CONNECTED_OTHER_PASSWORD_DEFAULT"}, + {2202, L"ERROR_BAD_USERNAME"}, + {2250, L"ERROR_NOT_CONNECTED"}, + {2401, L"ERROR_OPEN_FILES"}, + {2402, L"ERROR_ACTIVE_CONNECTIONS"}, + {2404, L"ERROR_DEVICE_IN_USE"}, + {3000, L"ERROR_UNKNOWN_PRINT_MONITOR"}, + {3001, L"ERROR_PRINTER_DRIVER_IN_USE"}, + {3002, L"ERROR_SPOOL_FILE_NOT_FOUND"}, + {3003, L"ERROR_SPL_NO_STARTDOC"}, + {3004, L"ERROR_SPL_NO_ADDJOB"}, + {3005, L"ERROR_PRINT_PROCESSOR_ALREADY_INSTALLED"}, + {3006, L"ERROR_PRINT_MONITOR_ALREADY_INSTALLED"}, + {3007, L"ERROR_INVALID_PRINT_MONITOR"}, + {3008, L"ERROR_PRINT_MONITOR_IN_USE"}, + {3009, L"ERROR_PRINTER_HAS_JOBS_QUEUED"}, + {3010, L"ERROR_SUCCESS_REBOOT_REQUIRED"}, + {3011, L"ERROR_SUCCESS_RESTART_REQUIRED"}, + {3012, L"ERROR_PRINTER_NOT_FOUND"}, + {3013, L"ERROR_PRINTER_DRIVER_WARNED"}, + {3014, L"ERROR_PRINTER_DRIVER_BLOCKED"}, + {3015, L"ERROR_PRINTER_DRIVER_PACKAGE_IN_USE"}, + {3016, L"ERROR_CORE_DRIVER_PACKAGE_NOT_FOUND"}, + {3017, L"ERROR_FAIL_REBOOT_REQUIRED"}, + {3018, L"ERROR_FAIL_REBOOT_INITIATED"}, + {3019, L"ERROR_PRINTER_DRIVER_DOWNLOAD_NEEDED"}, + {3020, L"ERROR_PRINT_JOB_RESTART_REQUIRED"}, + {3021, L"ERROR_INVALID_PRINTER_DRIVER_MANIFEST"}, + {3022, L"ERROR_PRINTER_NOT_SHAREABLE"}, + {3050, L"ERROR_REQUEST_PAUSED"}, + {3950, L"ERROR_IO_REISSUE_AS_CACHED"}, + {4000, L"ERROR_WINS_INTERNAL"}, + {4001, L"ERROR_CAN_NOT_DEL_LOCAL_WINS"}, + {4002, L"ERROR_STATIC_INIT"}, + {4003, L"ERROR_INC_BACKUP"}, + {4004, L"ERROR_FULL_BACKUP"}, + {4005, L"ERROR_REC_NON_EXISTENT"}, + {4006, L"ERROR_RPL_NOT_ALLOWED"}, + {4050, L"PEERDIST_ERROR_CONTENTINFO_VERSION_UNSUPPORTED"}, + {4051, L"PEERDIST_ERROR_CANNOT_PARSE_CONTENTINFO"}, + {4052, L"PEERDIST_ERROR_MISSING_DATA"}, + {4053, L"PEERDIST_ERROR_NO_MORE"}, + {4054, L"PEERDIST_ERROR_NOT_INITIALIZED"}, + {4055, L"PEERDIST_ERROR_ALREADY_INITIALIZED"}, + {4056, L"PEERDIST_ERROR_SHUTDOWN_IN_PROGRESS"}, + {4057, L"PEERDIST_ERROR_INVALIDATED"}, + {4058, L"PEERDIST_ERROR_ALREADY_EXISTS"}, + {4059, L"PEERDIST_ERROR_OPERATION_NOTFOUND"}, + {4060, L"PEERDIST_ERROR_ALREADY_COMPLETED"}, + {4061, L"PEERDIST_ERROR_OUT_OF_BOUNDS"}, + {4062, L"PEERDIST_ERROR_VERSION_UNSUPPORTED"}, + {4063, L"PEERDIST_ERROR_INVALID_CONFIGURATION"}, + {4064, L"PEERDIST_ERROR_NOT_LICENSED"}, + {4065, L"PEERDIST_ERROR_SERVICE_UNAVAILABLE"}, + {4066, L"PEERDIST_ERROR_TRUST_FAILURE"}, + {4100, L"ERROR_DHCP_ADDRESS_CONFLICT"}, + {4200, L"ERROR_WMI_GUID_NOT_FOUND"}, + {4201, L"ERROR_WMI_INSTANCE_NOT_FOUND"}, + {4202, L"ERROR_WMI_ITEMID_NOT_FOUND"}, + {4203, L"ERROR_WMI_TRY_AGAIN"}, + {4204, L"ERROR_WMI_DP_NOT_FOUND"}, + {4205, L"ERROR_WMI_UNRESOLVED_INSTANCE_REF"}, + {4206, L"ERROR_WMI_ALREADY_ENABLED"}, + {4207, L"ERROR_WMI_GUID_DISCONNECTED"}, + {4208, L"ERROR_WMI_SERVER_UNAVAILABLE"}, + {4209, L"ERROR_WMI_DP_FAILED"}, + {4210, L"ERROR_WMI_INVALID_MOF"}, + {4211, L"ERROR_WMI_INVALID_REGINFO"}, + {4212, L"ERROR_WMI_ALREADY_DISABLED"}, + {4213, L"ERROR_WMI_READ_ONLY"}, + {4214, L"ERROR_WMI_SET_FAILURE"}, + {4250, L"ERROR_NOT_APPCONTAINER"}, + {4251, L"ERROR_APPCONTAINER_REQUIRED"}, + {4252, L"ERROR_NOT_SUPPORTED_IN_APPCONTAINER"}, + {4253, L"ERROR_INVALID_PACKAGE_SID_LENGTH"}, + {4300, L"ERROR_INVALID_MEDIA"}, + {4301, L"ERROR_INVALID_LIBRARY"}, + {4302, L"ERROR_INVALID_MEDIA_POOL"}, + {4303, L"ERROR_DRIVE_MEDIA_MISMATCH"}, + {4304, L"ERROR_MEDIA_OFFLINE"}, + {4305, L"ERROR_LIBRARY_OFFLINE"}, + {4306, L"ERROR_EMPTY"}, + {4307, L"ERROR_NOT_EMPTY"}, + {4308, L"ERROR_MEDIA_UNAVAILABLE"}, + {4309, L"ERROR_RESOURCE_DISABLED"}, + {4310, L"ERROR_INVALID_CLEANER"}, + {4311, L"ERROR_UNABLE_TO_CLEAN"}, + {4312, L"ERROR_OBJECT_NOT_FOUND"}, + {4313, L"ERROR_DATABASE_FAILURE"}, + {4314, L"ERROR_DATABASE_FULL"}, + {4315, L"ERROR_MEDIA_INCOMPATIBLE"}, + {4316, L"ERROR_RESOURCE_NOT_PRESENT"}, + {4317, L"ERROR_INVALID_OPERATION"}, + {4318, L"ERROR_MEDIA_NOT_AVAILABLE"}, + {4319, L"ERROR_DEVICE_NOT_AVAILABLE"}, + {4320, L"ERROR_REQUEST_REFUSED"}, + {4321, L"ERROR_INVALID_DRIVE_OBJECT"}, + {4322, L"ERROR_LIBRARY_FULL"}, + {4323, L"ERROR_MEDIUM_NOT_ACCESSIBLE"}, + {4324, L"ERROR_UNABLE_TO_LOAD_MEDIUM"}, + {4325, L"ERROR_UNABLE_TO_INVENTORY_DRIVE"}, + {4326, L"ERROR_UNABLE_TO_INVENTORY_SLOT"}, + {4327, L"ERROR_UNABLE_TO_INVENTORY_TRANSPORT"}, + {4328, L"ERROR_TRANSPORT_FULL"}, + {4329, L"ERROR_CONTROLLING_IEPORT"}, + {4330, L"ERROR_UNABLE_TO_EJECT_MOUNTED_MEDIA"}, + {4331, L"ERROR_CLEANER_SLOT_SET"}, + {4332, L"ERROR_CLEANER_SLOT_NOT_SET"}, + {4333, L"ERROR_CLEANER_CARTRIDGE_SPENT"}, + {4334, L"ERROR_UNEXPECTED_OMID"}, + {4335, L"ERROR_CANT_DELETE_LAST_ITEM"}, + {4336, L"ERROR_MESSAGE_EXCEEDS_MAX_SIZE"}, + {4337, L"ERROR_VOLUME_CONTAINS_SYS_FILES"}, + {4338, L"ERROR_INDIGENOUS_TYPE"}, + {4339, L"ERROR_NO_SUPPORTING_DRIVES"}, + {4340, L"ERROR_CLEANER_CARTRIDGE_INSTALLED"}, + {4341, L"ERROR_IEPORT_FULL"}, + {4350, L"ERROR_FILE_OFFLINE"}, + {4351, L"ERROR_REMOTE_STORAGE_NOT_ACTIVE"}, + {4352, L"ERROR_REMOTE_STORAGE_MEDIA_ERROR"}, + {4390, L"ERROR_NOT_A_REPARSE_POINT"}, + {4391, L"ERROR_REPARSE_ATTRIBUTE_CONFLICT"}, + {4392, L"ERROR_INVALID_REPARSE_DATA"}, + {4393, L"ERROR_REPARSE_TAG_INVALID"}, + {4394, L"ERROR_REPARSE_TAG_MISMATCH"}, + {4400, L"ERROR_APP_DATA_NOT_FOUND"}, + {4401, L"ERROR_APP_DATA_EXPIRED"}, + {4402, L"ERROR_APP_DATA_CORRUPT"}, + {4403, L"ERROR_APP_DATA_LIMIT_EXCEEDED"}, + {4404, L"ERROR_APP_DATA_REBOOT_REQUIRED"}, + {4420, L"ERROR_SECUREBOOT_ROLLBACK_DETECTED"}, + {4421, L"ERROR_SECUREBOOT_POLICY_VIOLATION"}, + {4422, L"ERROR_SECUREBOOT_INVALID_POLICY"}, + {4423, L"ERROR_SECUREBOOT_POLICY_PUBLISHER_NOT_FOUND"}, + {4424, L"ERROR_SECUREBOOT_POLICY_NOT_SIGNED"}, + {4425, L"ERROR_SECUREBOOT_NOT_ENABLED"}, + {4426, L"ERROR_SECUREBOOT_FILE_REPLACED"}, + {4440, L"ERROR_OFFLOAD_READ_FLT_NOT_SUPPORTED"}, + {4441, L"ERROR_OFFLOAD_WRITE_FLT_NOT_SUPPORTED"}, + {4442, L"ERROR_OFFLOAD_READ_FILE_NOT_SUPPORTED"}, + {4443, L"ERROR_OFFLOAD_WRITE_FILE_NOT_SUPPORTED"}, + {4500, L"ERROR_VOLUME_NOT_SIS_ENABLED"}, + {5001, L"ERROR_DEPENDENT_RESOURCE_EXISTS"}, + {5002, L"ERROR_DEPENDENCY_NOT_FOUND"}, + {5003, L"ERROR_DEPENDENCY_ALREADY_EXISTS"}, + {5004, L"ERROR_RESOURCE_NOT_ONLINE"}, + {5005, L"ERROR_HOST_NODE_NOT_AVAILABLE"}, + {5006, L"ERROR_RESOURCE_NOT_AVAILABLE"}, + {5007, L"ERROR_RESOURCE_NOT_FOUND"}, + {5008, L"ERROR_SHUTDOWN_CLUSTER"}, + {5009, L"ERROR_CANT_EVICT_ACTIVE_NODE"}, + {5010, L"ERROR_OBJECT_ALREADY_EXISTS"}, + {5011, L"ERROR_OBJECT_IN_LIST"}, + {5012, L"ERROR_GROUP_NOT_AVAILABLE"}, + {5013, L"ERROR_GROUP_NOT_FOUND"}, + {5014, L"ERROR_GROUP_NOT_ONLINE"}, + {5015, L"ERROR_HOST_NODE_NOT_RESOURCE_OWNER"}, + {5016, L"ERROR_HOST_NODE_NOT_GROUP_OWNER"}, + {5017, L"ERROR_RESMON_CREATE_FAILED"}, + {5018, L"ERROR_RESMON_ONLINE_FAILED"}, + {5019, L"ERROR_RESOURCE_ONLINE"}, + {5020, L"ERROR_QUORUM_RESOURCE"}, + {5021, L"ERROR_NOT_QUORUM_CAPABLE"}, + {5022, L"ERROR_CLUSTER_SHUTTING_DOWN"}, + {5023, L"ERROR_INVALID_STATE"}, + {5024, L"ERROR_RESOURCE_PROPERTIES_STORED"}, + {5025, L"ERROR_NOT_QUORUM_CLASS"}, + {5026, L"ERROR_CORE_RESOURCE"}, + {5027, L"ERROR_QUORUM_RESOURCE_ONLINE_FAILED"}, + {5028, L"ERROR_QUORUMLOG_OPEN_FAILED"}, + {5029, L"ERROR_CLUSTERLOG_CORRUPT"}, + {5030, L"ERROR_CLUSTERLOG_RECORD_EXCEEDS_MAXSIZE"}, + {5031, L"ERROR_CLUSTERLOG_EXCEEDS_MAXSIZE"}, + {5032, L"ERROR_CLUSTERLOG_CHKPOINT_NOT_FOUND"}, + {5033, L"ERROR_CLUSTERLOG_NOT_ENOUGH_SPACE"}, + {5034, L"ERROR_QUORUM_OWNER_ALIVE"}, + {5035, L"ERROR_NETWORK_NOT_AVAILABLE"}, + {5036, L"ERROR_NODE_NOT_AVAILABLE"}, + {5037, L"ERROR_ALL_NODES_NOT_AVAILABLE"}, + {5038, L"ERROR_RESOURCE_FAILED"}, + {5039, L"ERROR_CLUSTER_INVALID_NODE"}, + {5040, L"ERROR_CLUSTER_NODE_EXISTS"}, + {5041, L"ERROR_CLUSTER_JOIN_IN_PROGRESS"}, + {5042, L"ERROR_CLUSTER_NODE_NOT_FOUND"}, + {5043, L"ERROR_CLUSTER_LOCAL_NODE_NOT_FOUND"}, + {5044, L"ERROR_CLUSTER_NETWORK_EXISTS"}, + {5045, L"ERROR_CLUSTER_NETWORK_NOT_FOUND"}, + {5046, L"ERROR_CLUSTER_NETINTERFACE_EXISTS"}, + {5047, L"ERROR_CLUSTER_NETINTERFACE_NOT_FOUND"}, + {5048, L"ERROR_CLUSTER_INVALID_REQUEST"}, + {5049, L"ERROR_CLUSTER_INVALID_NETWORK_PROVIDER"}, + {5050, L"ERROR_CLUSTER_NODE_DOWN"}, + {5051, L"ERROR_CLUSTER_NODE_UNREACHABLE"}, + {5052, L"ERROR_CLUSTER_NODE_NOT_MEMBER"}, + {5053, L"ERROR_CLUSTER_JOIN_NOT_IN_PROGRESS"}, + {5054, L"ERROR_CLUSTER_INVALID_NETWORK"}, + {5056, L"ERROR_CLUSTER_NODE_UP"}, + {5057, L"ERROR_CLUSTER_IPADDR_IN_USE"}, + {5058, L"ERROR_CLUSTER_NODE_NOT_PAUSED"}, + {5059, L"ERROR_CLUSTER_NO_SECURITY_CONTEXT"}, + {5060, L"ERROR_CLUSTER_NETWORK_NOT_INTERNAL"}, + {5061, L"ERROR_CLUSTER_NODE_ALREADY_UP"}, + {5062, L"ERROR_CLUSTER_NODE_ALREADY_DOWN"}, + {5063, L"ERROR_CLUSTER_NETWORK_ALREADY_ONLINE"}, + {5064, L"ERROR_CLUSTER_NETWORK_ALREADY_OFFLINE"}, + {5065, L"ERROR_CLUSTER_NODE_ALREADY_MEMBER"}, + {5066, L"ERROR_CLUSTER_LAST_INTERNAL_NETWORK"}, + {5067, L"ERROR_CLUSTER_NETWORK_HAS_DEPENDENTS"}, + {5068, L"ERROR_INVALID_OPERATION_ON_QUORUM"}, + {5069, L"ERROR_DEPENDENCY_NOT_ALLOWED"}, + {5070, L"ERROR_CLUSTER_NODE_PAUSED"}, + {5071, L"ERROR_NODE_CANT_HOST_RESOURCE"}, + {5072, L"ERROR_CLUSTER_NODE_NOT_READY"}, + {5073, L"ERROR_CLUSTER_NODE_SHUTTING_DOWN"}, + {5074, L"ERROR_CLUSTER_JOIN_ABORTED"}, + {5075, L"ERROR_CLUSTER_INCOMPATIBLE_VERSIONS"}, + {5076, L"ERROR_CLUSTER_MAXNUM_OF_RESOURCES_EXCEEDED"}, + {5077, L"ERROR_CLUSTER_SYSTEM_CONFIG_CHANGED"}, + {5078, L"ERROR_CLUSTER_RESOURCE_TYPE_NOT_FOUND"}, + {5079, L"ERROR_CLUSTER_RESTYPE_NOT_SUPPORTED"}, + {5080, L"ERROR_CLUSTER_RESNAME_NOT_FOUND"}, + {5081, L"ERROR_CLUSTER_NO_RPC_PACKAGES_REGISTERED"}, + {5082, L"ERROR_CLUSTER_OWNER_NOT_IN_PREFLIST"}, + {5083, L"ERROR_CLUSTER_DATABASE_SEQMISMATCH"}, + {5084, L"ERROR_RESMON_INVALID_STATE"}, + {5085, L"ERROR_CLUSTER_GUM_NOT_LOCKER"}, + {5086, L"ERROR_QUORUM_DISK_NOT_FOUND"}, + {5087, L"ERROR_DATABASE_BACKUP_CORRUPT"}, + {5088, L"ERROR_CLUSTER_NODE_ALREADY_HAS_DFS_ROOT"}, + {5089, L"ERROR_RESOURCE_PROPERTY_UNCHANGEABLE"}, + {5890, L"ERROR_CLUSTER_MEMBERSHIP_INVALID_STATE"}, + {5891, L"ERROR_CLUSTER_QUORUMLOG_NOT_FOUND"}, + {5892, L"ERROR_CLUSTER_MEMBERSHIP_HALT"}, + {5893, L"ERROR_CLUSTER_INSTANCE_ID_MISMATCH"}, + {5894, L"ERROR_CLUSTER_NETWORK_NOT_FOUND_FOR_IP"}, + {5895, L"ERROR_CLUSTER_PROPERTY_DATA_TYPE_MISMATCH"}, + {5896, L"ERROR_CLUSTER_EVICT_WITHOUT_CLEANUP"}, + {5897, L"ERROR_CLUSTER_PARAMETER_MISMATCH"}, + {5898, L"ERROR_NODE_CANNOT_BE_CLUSTERED"}, + {5899, L"ERROR_CLUSTER_WRONG_OS_VERSION"}, + {5900, L"ERROR_CLUSTER_CANT_CREATE_DUP_CLUSTER_NAME"}, + {5901, L"ERROR_CLUSCFG_ALREADY_COMMITTED"}, + {5902, L"ERROR_CLUSCFG_ROLLBACK_FAILED"}, + {5903, L"ERROR_CLUSCFG_SYSTEM_DISK_DRIVE_LETTER_CONFLICT"}, + {5904, L"ERROR_CLUSTER_OLD_VERSION"}, + {5905, L"ERROR_CLUSTER_MISMATCHED_COMPUTER_ACCT_NAME"}, + {5906, L"ERROR_CLUSTER_NO_NET_ADAPTERS"}, + {5907, L"ERROR_CLUSTER_POISONED"}, + {5908, L"ERROR_CLUSTER_GROUP_MOVING"}, + {5909, L"ERROR_CLUSTER_RESOURCE_TYPE_BUSY"}, + {5910, L"ERROR_RESOURCE_CALL_TIMED_OUT"}, + {5911, L"ERROR_INVALID_CLUSTER_IPV6_ADDRESS"}, + {5912, L"ERROR_CLUSTER_INTERNAL_INVALID_FUNCTION"}, + {5913, L"ERROR_CLUSTER_PARAMETER_OUT_OF_BOUNDS"}, + {5914, L"ERROR_CLUSTER_PARTIAL_SEND"}, + {5915, L"ERROR_CLUSTER_REGISTRY_INVALID_FUNCTION"}, + {5916, L"ERROR_CLUSTER_INVALID_STRING_TERMINATION"}, + {5917, L"ERROR_CLUSTER_INVALID_STRING_FORMAT"}, + {5918, L"ERROR_CLUSTER_DATABASE_TRANSACTION_IN_PROGRESS"}, + {5919, L"ERROR_CLUSTER_DATABASE_TRANSACTION_NOT_IN_PROGRESS"}, + {5920, L"ERROR_CLUSTER_NULL_DATA"}, + {5921, L"ERROR_CLUSTER_PARTIAL_READ"}, + {5922, L"ERROR_CLUSTER_PARTIAL_WRITE"}, + {5923, L"ERROR_CLUSTER_CANT_DESERIALIZE_DATA"}, + {5924, L"ERROR_DEPENDENT_RESOURCE_PROPERTY_CONFLICT"}, + {5925, L"ERROR_CLUSTER_NO_QUORUM"}, + {5926, L"ERROR_CLUSTER_INVALID_IPV6_NETWORK"}, + {5927, L"ERROR_CLUSTER_INVALID_IPV6_TUNNEL_NETWORK"}, + {5928, L"ERROR_QUORUM_NOT_ALLOWED_IN_THIS_GROUP"}, + {5929, L"ERROR_DEPENDENCY_TREE_TOO_COMPLEX"}, + {5930, L"ERROR_EXCEPTION_IN_RESOURCE_CALL"}, + {5931, L"ERROR_CLUSTER_RHS_FAILED_INITIALIZATION"}, + {5932, L"ERROR_CLUSTER_NOT_INSTALLED"}, + {5933, L"ERROR_CLUSTER_RESOURCES_MUST_BE_ONLINE_ON_THE_SAME_NODE"}, + {5934, L"ERROR_CLUSTER_MAX_NODES_IN_CLUSTER"}, + {5935, L"ERROR_CLUSTER_TOO_MANY_NODES"}, + {5936, L"ERROR_CLUSTER_OBJECT_ALREADY_USED"}, + {5937, L"ERROR_NONCORE_GROUPS_FOUND"}, + {5938, L"ERROR_FILE_SHARE_RESOURCE_CONFLICT"}, + {5939, L"ERROR_CLUSTER_EVICT_INVALID_REQUEST"}, + {5940, L"ERROR_CLUSTER_SINGLETON_RESOURCE"}, + {5941, L"ERROR_CLUSTER_GROUP_SINGLETON_RESOURCE"}, + {5942, L"ERROR_CLUSTER_RESOURCE_PROVIDER_FAILED"}, + {5943, L"ERROR_CLUSTER_RESOURCE_CONFIGURATION_ERROR"}, + {5944, L"ERROR_CLUSTER_GROUP_BUSY"}, + {5945, L"ERROR_CLUSTER_NOT_SHARED_VOLUME"}, + {5946, L"ERROR_CLUSTER_INVALID_SECURITY_DESCRIPTOR"}, + {5947, L"ERROR_CLUSTER_SHARED_VOLUMES_IN_USE"}, + {5948, L"ERROR_CLUSTER_USE_SHARED_VOLUMES_API"}, + {5949, L"ERROR_CLUSTER_BACKUP_IN_PROGRESS"}, + {5950, L"ERROR_NON_CSV_PATH"}, + {5951, L"ERROR_CSV_VOLUME_NOT_LOCAL"}, + {5952, L"ERROR_CLUSTER_WATCHDOG_TERMINATING"}, + {5953, L"ERROR_CLUSTER_RESOURCE_VETOED_MOVE_INCOMPATIBLE_NODES"}, + {5954, L"ERROR_CLUSTER_INVALID_NODE_WEIGHT"}, + {5955, L"ERROR_CLUSTER_RESOURCE_VETOED_CALL"}, + {5956, L"ERROR_RESMON_SYSTEM_RESOURCES_LACKING"}, + {5957, L"ERROR_CLUSTER_RESOURCE_VETOED_MOVE_NOT_ENOUGH_RESOURCES_ON_DESTINATION"}, + {5958, L"ERROR_CLUSTER_RESOURCE_VETOED_MOVE_NOT_ENOUGH_RESOURCES_ON_SOURCE"}, + {5959, L"ERROR_CLUSTER_GROUP_QUEUED"}, + {5960, L"ERROR_CLUSTER_RESOURCE_LOCKED_STATUS"}, + {5961, L"ERROR_CLUSTER_SHARED_VOLUME_FAILOVER_NOT_ALLOWED"}, + {5962, L"ERROR_CLUSTER_NODE_DRAIN_IN_PROGRESS"}, + {5963, L"ERROR_CLUSTER_DISK_NOT_CONNECTED"}, + {5964, L"ERROR_DISK_NOT_CSV_CAPABLE"}, + {5965, L"ERROR_RESOURCE_NOT_IN_AVAILABLE_STORAGE"}, + {5966, L"ERROR_CLUSTER_SHARED_VOLUME_REDIRECTED"}, + {5967, L"ERROR_CLUSTER_SHARED_VOLUME_NOT_REDIRECTED"}, + {5968, L"ERROR_CLUSTER_CANNOT_RETURN_PROPERTIES"}, + {5969, L"ERROR_CLUSTER_RESOURCE_CONTAINS_UNSUPPORTED_DIFF_AREA_FOR_SHARED_VOLUMES"}, + {5970, L"ERROR_CLUSTER_RESOURCE_IS_IN_MAINTENANCE_MODE"}, + {5971, L"ERROR_CLUSTER_AFFINITY_CONFLICT"}, + {5972, L"ERROR_CLUSTER_RESOURCE_IS_REPLICA_VIRTUAL_MACHINE"}, + {6000, L"ERROR_ENCRYPTION_FAILED"}, + {6001, L"ERROR_DECRYPTION_FAILED"}, + {6002, L"ERROR_FILE_ENCRYPTED"}, + {6003, L"ERROR_NO_RECOVERY_POLICY"}, + {6004, L"ERROR_NO_EFS"}, + {6005, L"ERROR_WRONG_EFS"}, + {6006, L"ERROR_NO_USER_KEYS"}, + {6007, L"ERROR_FILE_NOT_ENCRYPTED"}, + {6008, L"ERROR_NOT_EXPORT_FORMAT"}, + {6009, L"ERROR_FILE_READ_ONLY"}, + {6010, L"ERROR_DIR_EFS_DISALLOWED"}, + {6011, L"ERROR_EFS_SERVER_NOT_TRUSTED"}, + {6012, L"ERROR_BAD_RECOVERY_POLICY"}, + {6013, L"ERROR_EFS_ALG_BLOB_TOO_BIG"}, + {6014, L"ERROR_VOLUME_NOT_SUPPORT_EFS"}, + {6015, L"ERROR_EFS_DISABLED"}, + {6016, L"ERROR_EFS_VERSION_NOT_SUPPORT"}, + {6017, L"ERROR_CS_ENCRYPTION_INVALID_SERVER_RESPONSE"}, + {6018, L"ERROR_CS_ENCRYPTION_UNSUPPORTED_SERVER"}, + {6019, L"ERROR_CS_ENCRYPTION_EXISTING_ENCRYPTED_FILE"}, + {6020, L"ERROR_CS_ENCRYPTION_NEW_ENCRYPTED_FILE"}, + {6021, L"ERROR_CS_ENCRYPTION_FILE_NOT_CSE"}, + {6022, L"ERROR_ENCRYPTION_POLICY_DENIES_OPERATION"}, + {6118, L"ERROR_NO_BROWSER_SERVERS_FOUND"}, + {6200, L"SCHED_E_SERVICE_NOT_LOCALSYSTEM"}, + {6600, L"ERROR_LOG_SECTOR_INVALID"}, + {6601, L"ERROR_LOG_SECTOR_PARITY_INVALID"}, + {6602, L"ERROR_LOG_SECTOR_REMAPPED"}, + {6603, L"ERROR_LOG_BLOCK_INCOMPLETE"}, + {6604, L"ERROR_LOG_INVALID_RANGE"}, + {6605, L"ERROR_LOG_BLOCKS_EXHAUSTED"}, + {6606, L"ERROR_LOG_READ_CONTEXT_INVALID"}, + {6607, L"ERROR_LOG_RESTART_INVALID"}, + {6608, L"ERROR_LOG_BLOCK_VERSION"}, + {6609, L"ERROR_LOG_BLOCK_INVALID"}, + {6610, L"ERROR_LOG_READ_MODE_INVALID"}, + {6611, L"ERROR_LOG_NO_RESTART"}, + {6612, L"ERROR_LOG_METADATA_CORRUPT"}, + {6613, L"ERROR_LOG_METADATA_INVALID"}, + {6614, L"ERROR_LOG_METADATA_INCONSISTENT"}, + {6615, L"ERROR_LOG_RESERVATION_INVALID"}, + {6616, L"ERROR_LOG_CANT_DELETE"}, + {6617, L"ERROR_LOG_CONTAINER_LIMIT_EXCEEDED"}, + {6618, L"ERROR_LOG_START_OF_LOG"}, + {6619, L"ERROR_LOG_POLICY_ALREADY_INSTALLED"}, + {6620, L"ERROR_LOG_POLICY_NOT_INSTALLED"}, + {6621, L"ERROR_LOG_POLICY_INVALID"}, + {6622, L"ERROR_LOG_POLICY_CONFLICT"}, + {6623, L"ERROR_LOG_PINNED_ARCHIVE_TAIL"}, + {6624, L"ERROR_LOG_RECORD_NONEXISTENT"}, + {6625, L"ERROR_LOG_RECORDS_RESERVED_INVALID"}, + {6626, L"ERROR_LOG_SPACE_RESERVED_INVALID"}, + {6627, L"ERROR_LOG_TAIL_INVALID"}, + {6628, L"ERROR_LOG_FULL"}, + {6629, L"ERROR_COULD_NOT_RESIZE_LOG"}, + {6630, L"ERROR_LOG_MULTIPLEXED"}, + {6631, L"ERROR_LOG_DEDICATED"}, + {6632, L"ERROR_LOG_ARCHIVE_NOT_IN_PROGRESS"}, + {6633, L"ERROR_LOG_ARCHIVE_IN_PROGRESS"}, + {6634, L"ERROR_LOG_EPHEMERAL"}, + {6635, L"ERROR_LOG_NOT_ENOUGH_CONTAINERS"}, + {6636, L"ERROR_LOG_CLIENT_ALREADY_REGISTERED"}, + {6637, L"ERROR_LOG_CLIENT_NOT_REGISTERED"}, + {6638, L"ERROR_LOG_FULL_HANDLER_IN_PROGRESS"}, + {6639, L"ERROR_LOG_CONTAINER_READ_FAILED"}, + {6640, L"ERROR_LOG_CONTAINER_WRITE_FAILED"}, + {6641, L"ERROR_LOG_CONTAINER_OPEN_FAILED"}, + {6642, L"ERROR_LOG_CONTAINER_STATE_INVALID"}, + {6643, L"ERROR_LOG_STATE_INVALID"}, + {6644, L"ERROR_LOG_PINNED"}, + {6645, L"ERROR_LOG_METADATA_FLUSH_FAILED"}, + {6646, L"ERROR_LOG_INCONSISTENT_SECURITY"}, + {6647, L"ERROR_LOG_APPENDED_FLUSH_FAILED"}, + {6648, L"ERROR_LOG_PINNED_RESERVATION"}, + {6700, L"ERROR_INVALID_TRANSACTION"}, + {6701, L"ERROR_TRANSACTION_NOT_ACTIVE"}, + {6702, L"ERROR_TRANSACTION_REQUEST_NOT_VALID"}, + {6703, L"ERROR_TRANSACTION_NOT_REQUESTED"}, + {6704, L"ERROR_TRANSACTION_ALREADY_ABORTED"}, + {6705, L"ERROR_TRANSACTION_ALREADY_COMMITTED"}, + {6706, L"ERROR_TM_INITIALIZATION_FAILED"}, + {6707, L"ERROR_RESOURCEMANAGER_READ_ONLY"}, + {6708, L"ERROR_TRANSACTION_NOT_JOINED"}, + {6709, L"ERROR_TRANSACTION_SUPERIOR_EXISTS"}, + {6710, L"ERROR_CRM_PROTOCOL_ALREADY_EXISTS"}, + {6711, L"ERROR_TRANSACTION_PROPAGATION_FAILED"}, + {6712, L"ERROR_CRM_PROTOCOL_NOT_FOUND"}, + {6713, L"ERROR_TRANSACTION_INVALID_MARSHALL_BUFFER"}, + {6714, L"ERROR_CURRENT_TRANSACTION_NOT_VALID"}, + {6715, L"ERROR_TRANSACTION_NOT_FOUND"}, + {6716, L"ERROR_RESOURCEMANAGER_NOT_FOUND"}, + {6717, L"ERROR_ENLISTMENT_NOT_FOUND"}, + {6718, L"ERROR_TRANSACTIONMANAGER_NOT_FOUND"}, + {6719, L"ERROR_TRANSACTIONMANAGER_NOT_ONLINE"}, + {6720, L"ERROR_TRANSACTIONMANAGER_RECOVERY_NAME_COLLISION"}, + {6721, L"ERROR_TRANSACTION_NOT_ROOT"}, + {6722, L"ERROR_TRANSACTION_OBJECT_EXPIRED"}, + {6723, L"ERROR_TRANSACTION_RESPONSE_NOT_ENLISTED"}, + {6724, L"ERROR_TRANSACTION_RECORD_TOO_LONG"}, + {6725, L"ERROR_IMPLICIT_TRANSACTION_NOT_SUPPORTED"}, + {6726, L"ERROR_TRANSACTION_INTEGRITY_VIOLATED"}, + {6727, L"ERROR_TRANSACTIONMANAGER_IDENTITY_MISMATCH"}, + {6728, L"ERROR_RM_CANNOT_BE_FROZEN_FOR_SNAPSHOT"}, + {6729, L"ERROR_TRANSACTION_MUST_WRITETHROUGH"}, + {6730, L"ERROR_TRANSACTION_NO_SUPERIOR"}, + {6731, L"ERROR_HEURISTIC_DAMAGE_POSSIBLE"}, + {6800, L"ERROR_TRANSACTIONAL_CONFLICT"}, + {6801, L"ERROR_RM_NOT_ACTIVE"}, + {6802, L"ERROR_RM_METADATA_CORRUPT"}, + {6803, L"ERROR_DIRECTORY_NOT_RM"}, + {6805, L"ERROR_TRANSACTIONS_UNSUPPORTED_REMOTE"}, + {6806, L"ERROR_LOG_RESIZE_INVALID_SIZE"}, + {6807, L"ERROR_OBJECT_NO_LONGER_EXISTS"}, + {6808, L"ERROR_STREAM_MINIVERSION_NOT_FOUND"}, + {6809, L"ERROR_STREAM_MINIVERSION_NOT_VALID"}, + {6810, L"ERROR_MINIVERSION_INACCESSIBLE_FROM_SPECIFIED_TRANSACTION"}, + {6811, L"ERROR_CANT_OPEN_MINIVERSION_WITH_MODIFY_INTENT"}, + {6812, L"ERROR_CANT_CREATE_MORE_STREAM_MINIVERSIONS"}, + {6814, L"ERROR_REMOTE_FILE_VERSION_MISMATCH"}, + {6815, L"ERROR_HANDLE_NO_LONGER_VALID"}, + {6816, L"ERROR_NO_TXF_METADATA"}, + {6817, L"ERROR_LOG_CORRUPTION_DETECTED"}, + {6818, L"ERROR_CANT_RECOVER_WITH_HANDLE_OPEN"}, + {6819, L"ERROR_RM_DISCONNECTED"}, + {6820, L"ERROR_ENLISTMENT_NOT_SUPERIOR"}, + {6821, L"ERROR_RECOVERY_NOT_NEEDED"}, + {6822, L"ERROR_RM_ALREADY_STARTED"}, + {6823, L"ERROR_FILE_IDENTITY_NOT_PERSISTENT"}, + {6824, L"ERROR_CANT_BREAK_TRANSACTIONAL_DEPENDENCY"}, + {6825, L"ERROR_CANT_CROSS_RM_BOUNDARY"}, + {6826, L"ERROR_TXF_DIR_NOT_EMPTY"}, + {6827, L"ERROR_INDOUBT_TRANSACTIONS_EXIST"}, + {6828, L"ERROR_TM_VOLATILE"}, + {6829, L"ERROR_ROLLBACK_TIMER_EXPIRED"}, + {6830, L"ERROR_TXF_ATTRIBUTE_CORRUPT"}, + {6831, L"ERROR_EFS_NOT_ALLOWED_IN_TRANSACTION"}, + {6832, L"ERROR_TRANSACTIONAL_OPEN_NOT_ALLOWED"}, + {6833, L"ERROR_LOG_GROWTH_FAILED"}, + {6834, L"ERROR_TRANSACTED_MAPPING_UNSUPPORTED_REMOTE"}, + {6835, L"ERROR_TXF_METADATA_ALREADY_PRESENT"}, + {6836, L"ERROR_TRANSACTION_SCOPE_CALLBACKS_NOT_SET"}, + {6837, L"ERROR_TRANSACTION_REQUIRED_PROMOTION"}, + {6838, L"ERROR_CANNOT_EXECUTE_FILE_IN_TRANSACTION"}, + {6839, L"ERROR_TRANSACTIONS_NOT_FROZEN"}, + {6840, L"ERROR_TRANSACTION_FREEZE_IN_PROGRESS"}, + {6841, L"ERROR_NOT_SNAPSHOT_VOLUME"}, + {6842, L"ERROR_NO_SAVEPOINT_WITH_OPEN_FILES"}, + {6843, L"ERROR_DATA_LOST_REPAIR"}, + {6844, L"ERROR_SPARSE_NOT_ALLOWED_IN_TRANSACTION"}, + {6845, L"ERROR_TM_IDENTITY_MISMATCH"}, + {6846, L"ERROR_FLOATED_SECTION"}, + {6847, L"ERROR_CANNOT_ACCEPT_TRANSACTED_WORK"}, + {6848, L"ERROR_CANNOT_ABORT_TRANSACTIONS"}, + {6849, L"ERROR_BAD_CLUSTERS"}, + {6850, L"ERROR_COMPRESSION_NOT_ALLOWED_IN_TRANSACTION"}, + {6851, L"ERROR_VOLUME_DIRTY"}, + {6852, L"ERROR_NO_LINK_TRACKING_IN_TRANSACTION"}, + {6853, L"ERROR_OPERATION_NOT_SUPPORTED_IN_TRANSACTION"}, + {6854, L"ERROR_EXPIRED_HANDLE"}, + {6855, L"ERROR_TRANSACTION_NOT_ENLISTED"}, + {7001, L"ERROR_CTX_WINSTATION_NAME_INVALID"}, + {7002, L"ERROR_CTX_INVALID_PD"}, + {7003, L"ERROR_CTX_PD_NOT_FOUND"}, + {7004, L"ERROR_CTX_WD_NOT_FOUND"}, + {7005, L"ERROR_CTX_CANNOT_MAKE_EVENTLOG_ENTRY"}, + {7006, L"ERROR_CTX_SERVICE_NAME_COLLISION"}, + {7007, L"ERROR_CTX_CLOSE_PENDING"}, + {7008, L"ERROR_CTX_NO_OUTBUF"}, + {7009, L"ERROR_CTX_MODEM_INF_NOT_FOUND"}, + {7010, L"ERROR_CTX_INVALID_MODEMNAME"}, + {7011, L"ERROR_CTX_MODEM_RESPONSE_ERROR"}, + {7012, L"ERROR_CTX_MODEM_RESPONSE_TIMEOUT"}, + {7013, L"ERROR_CTX_MODEM_RESPONSE_NO_CARRIER"}, + {7014, L"ERROR_CTX_MODEM_RESPONSE_NO_DIALTONE"}, + {7015, L"ERROR_CTX_MODEM_RESPONSE_BUSY"}, + {7016, L"ERROR_CTX_MODEM_RESPONSE_VOICE"}, + {7017, L"ERROR_CTX_TD_ERROR"}, + {7022, L"ERROR_CTX_WINSTATION_NOT_FOUND"}, + {7023, L"ERROR_CTX_WINSTATION_ALREADY_EXISTS"}, + {7024, L"ERROR_CTX_WINSTATION_BUSY"}, + {7025, L"ERROR_CTX_BAD_VIDEO_MODE"}, + {7035, L"ERROR_CTX_GRAPHICS_INVALID"}, + {7037, L"ERROR_CTX_LOGON_DISABLED"}, + {7038, L"ERROR_CTX_NOT_CONSOLE"}, + {7040, L"ERROR_CTX_CLIENT_QUERY_TIMEOUT"}, + {7041, L"ERROR_CTX_CONSOLE_DISCONNECT"}, + {7042, L"ERROR_CTX_CONSOLE_CONNECT"}, + {7044, L"ERROR_CTX_SHADOW_DENIED"}, + {7045, L"ERROR_CTX_WINSTATION_ACCESS_DENIED"}, + {7049, L"ERROR_CTX_INVALID_WD"}, + {7050, L"ERROR_CTX_SHADOW_INVALID"}, + {7051, L"ERROR_CTX_SHADOW_DISABLED"}, + {7052, L"ERROR_CTX_CLIENT_LICENSE_IN_USE"}, + {7053, L"ERROR_CTX_CLIENT_LICENSE_NOT_SET"}, + {7054, L"ERROR_CTX_LICENSE_NOT_AVAILABLE"}, + {7055, L"ERROR_CTX_LICENSE_CLIENT_INVALID"}, + {7056, L"ERROR_CTX_LICENSE_EXPIRED"}, + {7057, L"ERROR_CTX_SHADOW_NOT_RUNNING"}, + {7058, L"ERROR_CTX_SHADOW_ENDED_BY_MODE_CHANGE"}, + {7059, L"ERROR_ACTIVATION_COUNT_EXCEEDED"}, + {7060, L"ERROR_CTX_WINSTATIONS_DISABLED"}, + {7061, L"ERROR_CTX_ENCRYPTION_LEVEL_REQUIRED"}, + {7062, L"ERROR_CTX_SESSION_IN_USE"}, + {7063, L"ERROR_CTX_NO_FORCE_LOGOFF"}, + {7064, L"ERROR_CTX_ACCOUNT_RESTRICTION"}, + {7065, L"ERROR_RDP_PROTOCOL_ERROR"}, + {7066, L"ERROR_CTX_CDM_CONNECT"}, + {7067, L"ERROR_CTX_CDM_DISCONNECT"}, + {7068, L"ERROR_CTX_SECURITY_LAYER_ERROR"}, + {7069, L"ERROR_TS_INCOMPATIBLE_SESSIONS"}, + {7070, L"ERROR_TS_VIDEO_SUBSYSTEM_ERROR"}, + {8001, L"FRS_ERR_INVALID_API_SEQUENCE"}, + {8002, L"FRS_ERR_STARTING_SERVICE"}, + {8003, L"FRS_ERR_STOPPING_SERVICE"}, + {8004, L"FRS_ERR_INTERNAL_API"}, + {8005, L"FRS_ERR_INTERNAL"}, + {8006, L"FRS_ERR_SERVICE_COMM"}, + {8007, L"FRS_ERR_INSUFFICIENT_PRIV"}, + {8008, L"FRS_ERR_AUTHENTICATION"}, + {8009, L"FRS_ERR_PARENT_INSUFFICIENT_PRIV"}, + {8010, L"FRS_ERR_PARENT_AUTHENTICATION"}, + {8011, L"FRS_ERR_CHILD_TO_PARENT_COMM"}, + {8012, L"FRS_ERR_PARENT_TO_CHILD_COMM"}, + {8013, L"FRS_ERR_SYSVOL_POPULATE"}, + {8014, L"FRS_ERR_SYSVOL_POPULATE_TIMEOUT"}, + {8015, L"FRS_ERR_SYSVOL_IS_BUSY"}, + {8016, L"FRS_ERR_SYSVOL_DEMOTE"}, + {8017, L"FRS_ERR_INVALID_SERVICE_PARAMETER"}, + {8200, L"ERROR_DS_NOT_INSTALLED"}, + {8201, L"ERROR_DS_MEMBERSHIP_EVALUATED_LOCALLY"}, + {8202, L"ERROR_DS_NO_ATTRIBUTE_OR_VALUE"}, + {8203, L"ERROR_DS_INVALID_ATTRIBUTE_SYNTAX"}, + {8204, L"ERROR_DS_ATTRIBUTE_TYPE_UNDEFINED"}, + {8205, L"ERROR_DS_ATTRIBUTE_OR_VALUE_EXISTS"}, + {8206, L"ERROR_DS_BUSY"}, + {8207, L"ERROR_DS_UNAVAILABLE"}, + {8208, L"ERROR_DS_NO_RIDS_ALLOCATED"}, + {8209, L"ERROR_DS_NO_MORE_RIDS"}, + {8210, L"ERROR_DS_INCORRECT_ROLE_OWNER"}, + {8211, L"ERROR_DS_RIDMGR_INIT_ERROR"}, + {8212, L"ERROR_DS_OBJ_CLASS_VIOLATION"}, + {8213, L"ERROR_DS_CANT_ON_NON_LEAF"}, + {8214, L"ERROR_DS_CANT_ON_RDN"}, + {8215, L"ERROR_DS_CANT_MOD_OBJ_CLASS"}, + {8216, L"ERROR_DS_CROSS_DOM_MOVE_ERROR"}, + {8217, L"ERROR_DS_GC_NOT_AVAILABLE"}, + {8218, L"ERROR_SHARED_POLICY"}, + {8219, L"ERROR_POLICY_OBJECT_NOT_FOUND"}, + {8220, L"ERROR_POLICY_ONLY_IN_DS"}, + {8221, L"ERROR_PROMOTION_ACTIVE"}, + {8222, L"ERROR_NO_PROMOTION_ACTIVE"}, + {8224, L"ERROR_DS_OPERATIONS_ERROR"}, + {8225, L"ERROR_DS_PROTOCOL_ERROR"}, + {8226, L"ERROR_DS_TIMELIMIT_EXCEEDED"}, + {8227, L"ERROR_DS_SIZELIMIT_EXCEEDED"}, + {8228, L"ERROR_DS_ADMIN_LIMIT_EXCEEDED"}, + {8229, L"ERROR_DS_COMPARE_FALSE"}, + {8230, L"ERROR_DS_COMPARE_TRUE"}, + {8231, L"ERROR_DS_AUTH_METHOD_NOT_SUPPORTED"}, + {8232, L"ERROR_DS_STRONG_AUTH_REQUIRED"}, + {8233, L"ERROR_DS_INAPPROPRIATE_AUTH"}, + {8234, L"ERROR_DS_AUTH_UNKNOWN"}, + {8235, L"ERROR_DS_REFERRAL"}, + {8236, L"ERROR_DS_UNAVAILABLE_CRIT_EXTENSION"}, + {8237, L"ERROR_DS_CONFIDENTIALITY_REQUIRED"}, + {8238, L"ERROR_DS_INAPPROPRIATE_MATCHING"}, + {8239, L"ERROR_DS_CONSTRAINT_VIOLATION"}, + {8240, L"ERROR_DS_NO_SUCH_OBJECT"}, + {8241, L"ERROR_DS_ALIAS_PROBLEM"}, + {8242, L"ERROR_DS_INVALID_DN_SYNTAX"}, + {8243, L"ERROR_DS_IS_LEAF"}, + {8244, L"ERROR_DS_ALIAS_DEREF_PROBLEM"}, + {8245, L"ERROR_DS_UNWILLING_TO_PERFORM"}, + {8246, L"ERROR_DS_LOOP_DETECT"}, + {8247, L"ERROR_DS_NAMING_VIOLATION"}, + {8248, L"ERROR_DS_OBJECT_RESULTS_TOO_LARGE"}, + {8249, L"ERROR_DS_AFFECTS_MULTIPLE_DSAS"}, + {8250, L"ERROR_DS_SERVER_DOWN"}, + {8251, L"ERROR_DS_LOCAL_ERROR"}, + {8252, L"ERROR_DS_ENCODING_ERROR"}, + {8253, L"ERROR_DS_DECODING_ERROR"}, + {8254, L"ERROR_DS_FILTER_UNKNOWN"}, + {8255, L"ERROR_DS_PARAM_ERROR"}, + {8256, L"ERROR_DS_NOT_SUPPORTED"}, + {8257, L"ERROR_DS_NO_RESULTS_RETURNED"}, + {8258, L"ERROR_DS_CONTROL_NOT_FOUND"}, + {8259, L"ERROR_DS_CLIENT_LOOP"}, + {8260, L"ERROR_DS_REFERRAL_LIMIT_EXCEEDED"}, + {8261, L"ERROR_DS_SORT_CONTROL_MISSING"}, + {8262, L"ERROR_DS_OFFSET_RANGE_ERROR"}, + {8263, L"ERROR_DS_RIDMGR_DISABLED"}, + {8301, L"ERROR_DS_ROOT_MUST_BE_NC"}, + {8302, L"ERROR_DS_ADD_REPLICA_INHIBITED"}, + {8303, L"ERROR_DS_ATT_NOT_DEF_IN_SCHEMA"}, + {8304, L"ERROR_DS_MAX_OBJ_SIZE_EXCEEDED"}, + {8305, L"ERROR_DS_OBJ_STRING_NAME_EXISTS"}, + {8306, L"ERROR_DS_NO_RDN_DEFINED_IN_SCHEMA"}, + {8307, L"ERROR_DS_RDN_DOESNT_MATCH_SCHEMA"}, + {8308, L"ERROR_DS_NO_REQUESTED_ATTS_FOUND"}, + {8309, L"ERROR_DS_USER_BUFFER_TO_SMALL"}, + {8310, L"ERROR_DS_ATT_IS_NOT_ON_OBJ"}, + {8311, L"ERROR_DS_ILLEGAL_MOD_OPERATION"}, + {8312, L"ERROR_DS_OBJ_TOO_LARGE"}, + {8313, L"ERROR_DS_BAD_INSTANCE_TYPE"}, + {8314, L"ERROR_DS_MASTERDSA_REQUIRED"}, + {8315, L"ERROR_DS_OBJECT_CLASS_REQUIRED"}, + {8316, L"ERROR_DS_MISSING_REQUIRED_ATT"}, + {8317, L"ERROR_DS_ATT_NOT_DEF_FOR_CLASS"}, + {8318, L"ERROR_DS_ATT_ALREADY_EXISTS"}, + {8320, L"ERROR_DS_CANT_ADD_ATT_VALUES"}, + {8321, L"ERROR_DS_SINGLE_VALUE_CONSTRAINT"}, + {8322, L"ERROR_DS_RANGE_CONSTRAINT"}, + {8323, L"ERROR_DS_ATT_VAL_ALREADY_EXISTS"}, + {8324, L"ERROR_DS_CANT_REM_MISSING_ATT"}, + {8325, L"ERROR_DS_CANT_REM_MISSING_ATT_VAL"}, + {8326, L"ERROR_DS_ROOT_CANT_BE_SUBREF"}, + {8327, L"ERROR_DS_NO_CHAINING"}, + {8328, L"ERROR_DS_NO_CHAINED_EVAL"}, + {8329, L"ERROR_DS_NO_PARENT_OBJECT"}, + {8330, L"ERROR_DS_PARENT_IS_AN_ALIAS"}, + {8331, L"ERROR_DS_CANT_MIX_MASTER_AND_REPS"}, + {8332, L"ERROR_DS_CHILDREN_EXIST"}, + {8333, L"ERROR_DS_OBJ_NOT_FOUND"}, + {8334, L"ERROR_DS_ALIASED_OBJ_MISSING"}, + {8335, L"ERROR_DS_BAD_NAME_SYNTAX"}, + {8336, L"ERROR_DS_ALIAS_POINTS_TO_ALIAS"}, + {8337, L"ERROR_DS_CANT_DEREF_ALIAS"}, + {8338, L"ERROR_DS_OUT_OF_SCOPE"}, + {8339, L"ERROR_DS_OBJECT_BEING_REMOVED"}, + {8340, L"ERROR_DS_CANT_DELETE_DSA_OBJ"}, + {8341, L"ERROR_DS_GENERIC_ERROR"}, + {8342, L"ERROR_DS_DSA_MUST_BE_INT_MASTER"}, + {8343, L"ERROR_DS_CLASS_NOT_DSA"}, + {8344, L"ERROR_DS_INSUFF_ACCESS_RIGHTS"}, + {8345, L"ERROR_DS_ILLEGAL_SUPERIOR"}, + {8346, L"ERROR_DS_ATTRIBUTE_OWNED_BY_SAM"}, + {8347, L"ERROR_DS_NAME_TOO_MANY_PARTS"}, + {8348, L"ERROR_DS_NAME_TOO_LONG"}, + {8349, L"ERROR_DS_NAME_VALUE_TOO_LONG"}, + {8350, L"ERROR_DS_NAME_UNPARSEABLE"}, + {8351, L"ERROR_DS_NAME_TYPE_UNKNOWN"}, + {8352, L"ERROR_DS_NOT_AN_OBJECT"}, + {8353, L"ERROR_DS_SEC_DESC_TOO_SHORT"}, + {8354, L"ERROR_DS_SEC_DESC_INVALID"}, + {8355, L"ERROR_DS_NO_DELETED_NAME"}, + {8356, L"ERROR_DS_SUBREF_MUST_HAVE_PARENT"}, + {8357, L"ERROR_DS_NCNAME_MUST_BE_NC"}, + {8358, L"ERROR_DS_CANT_ADD_SYSTEM_ONLY"}, + {8359, L"ERROR_DS_CLASS_MUST_BE_CONCRETE"}, + {8360, L"ERROR_DS_INVALID_DMD"}, + {8361, L"ERROR_DS_OBJ_GUID_EXISTS"}, + {8362, L"ERROR_DS_NOT_ON_BACKLINK"}, + {8363, L"ERROR_DS_NO_CROSSREF_FOR_NC"}, + {8364, L"ERROR_DS_SHUTTING_DOWN"}, + {8365, L"ERROR_DS_UNKNOWN_OPERATION"}, + {8366, L"ERROR_DS_INVALID_ROLE_OWNER"}, + {8367, L"ERROR_DS_COULDNT_CONTACT_FSMO"}, + {8368, L"ERROR_DS_CROSS_NC_DN_RENAME"}, + {8369, L"ERROR_DS_CANT_MOD_SYSTEM_ONLY"}, + {8370, L"ERROR_DS_REPLICATOR_ONLY"}, + {8371, L"ERROR_DS_OBJ_CLASS_NOT_DEFINED"}, + {8372, L"ERROR_DS_OBJ_CLASS_NOT_SUBCLASS"}, + {8373, L"ERROR_DS_NAME_REFERENCE_INVALID"}, + {8374, L"ERROR_DS_CROSS_REF_EXISTS"}, + {8375, L"ERROR_DS_CANT_DEL_MASTER_CROSSREF"}, + {8376, L"ERROR_DS_SUBTREE_NOTIFY_NOT_NC_HEAD"}, + {8377, L"ERROR_DS_NOTIFY_FILTER_TOO_COMPLEX"}, + {8378, L"ERROR_DS_DUP_RDN"}, + {8379, L"ERROR_DS_DUP_OID"}, + {8380, L"ERROR_DS_DUP_MAPI_ID"}, + {8381, L"ERROR_DS_DUP_SCHEMA_ID_GUID"}, + {8382, L"ERROR_DS_DUP_LDAP_DISPLAY_NAME"}, + {8383, L"ERROR_DS_SEMANTIC_ATT_TEST"}, + {8384, L"ERROR_DS_SYNTAX_MISMATCH"}, + {8385, L"ERROR_DS_EXISTS_IN_MUST_HAVE"}, + {8386, L"ERROR_DS_EXISTS_IN_MAY_HAVE"}, + {8387, L"ERROR_DS_NONEXISTENT_MAY_HAVE"}, + {8388, L"ERROR_DS_NONEXISTENT_MUST_HAVE"}, + {8389, L"ERROR_DS_AUX_CLS_TEST_FAIL"}, + {8390, L"ERROR_DS_NONEXISTENT_POSS_SUP"}, + {8391, L"ERROR_DS_SUB_CLS_TEST_FAIL"}, + {8392, L"ERROR_DS_BAD_RDN_ATT_ID_SYNTAX"}, + {8393, L"ERROR_DS_EXISTS_IN_AUX_CLS"}, + {8394, L"ERROR_DS_EXISTS_IN_SUB_CLS"}, + {8395, L"ERROR_DS_EXISTS_IN_POSS_SUP"}, + {8396, L"ERROR_DS_RECALCSCHEMA_FAILED"}, + {8397, L"ERROR_DS_TREE_DELETE_NOT_FINISHED"}, + {8398, L"ERROR_DS_CANT_DELETE"}, + {8399, L"ERROR_DS_ATT_SCHEMA_REQ_ID"}, + {8400, L"ERROR_DS_BAD_ATT_SCHEMA_SYNTAX"}, + {8401, L"ERROR_DS_CANT_CACHE_ATT"}, + {8402, L"ERROR_DS_CANT_CACHE_CLASS"}, + {8403, L"ERROR_DS_CANT_REMOVE_ATT_CACHE"}, + {8404, L"ERROR_DS_CANT_REMOVE_CLASS_CACHE"}, + {8405, L"ERROR_DS_CANT_RETRIEVE_DN"}, + {8406, L"ERROR_DS_MISSING_SUPREF"}, + {8407, L"ERROR_DS_CANT_RETRIEVE_INSTANCE"}, + {8408, L"ERROR_DS_CODE_INCONSISTENCY"}, + {8409, L"ERROR_DS_DATABASE_ERROR"}, + {8410, L"ERROR_DS_GOVERNSID_MISSING"}, + {8411, L"ERROR_DS_MISSING_EXPECTED_ATT"}, + {8412, L"ERROR_DS_NCNAME_MISSING_CR_REF"}, + {8413, L"ERROR_DS_SECURITY_CHECKING_ERROR"}, + {8414, L"ERROR_DS_SCHEMA_NOT_LOADED"}, + {8415, L"ERROR_DS_SCHEMA_ALLOC_FAILED"}, + {8416, L"ERROR_DS_ATT_SCHEMA_REQ_SYNTAX"}, + {8417, L"ERROR_DS_GCVERIFY_ERROR"}, + {8418, L"ERROR_DS_DRA_SCHEMA_MISMATCH"}, + {8419, L"ERROR_DS_CANT_FIND_DSA_OBJ"}, + {8420, L"ERROR_DS_CANT_FIND_EXPECTED_NC"}, + {8421, L"ERROR_DS_CANT_FIND_NC_IN_CACHE"}, + {8422, L"ERROR_DS_CANT_RETRIEVE_CHILD"}, + {8423, L"ERROR_DS_SECURITY_ILLEGAL_MODIFY"}, + {8424, L"ERROR_DS_CANT_REPLACE_HIDDEN_REC"}, + {8425, L"ERROR_DS_BAD_HIERARCHY_FILE"}, + {8426, L"ERROR_DS_BUILD_HIERARCHY_TABLE_FAILED"}, + {8427, L"ERROR_DS_CONFIG_PARAM_MISSING"}, + {8428, L"ERROR_DS_COUNTING_AB_INDICES_FAILED"}, + {8429, L"ERROR_DS_HIERARCHY_TABLE_MALLOC_FAILED"}, + {8430, L"ERROR_DS_INTERNAL_FAILURE"}, + {8431, L"ERROR_DS_UNKNOWN_ERROR"}, + {8432, L"ERROR_DS_ROOT_REQUIRES_CLASS_TOP"}, + {8433, L"ERROR_DS_REFUSING_FSMO_ROLES"}, + {8434, L"ERROR_DS_MISSING_FSMO_SETTINGS"}, + {8435, L"ERROR_DS_UNABLE_TO_SURRENDER_ROLES"}, + {8436, L"ERROR_DS_DRA_GENERIC"}, + {8437, L"ERROR_DS_DRA_INVALID_PARAMETER"}, + {8438, L"ERROR_DS_DRA_BUSY"}, + {8439, L"ERROR_DS_DRA_BAD_DN"}, + {8440, L"ERROR_DS_DRA_BAD_NC"}, + {8441, L"ERROR_DS_DRA_DN_EXISTS"}, + {8442, L"ERROR_DS_DRA_INTERNAL_ERROR"}, + {8443, L"ERROR_DS_DRA_INCONSISTENT_DIT"}, + {8444, L"ERROR_DS_DRA_CONNECTION_FAILED"}, + {8445, L"ERROR_DS_DRA_BAD_INSTANCE_TYPE"}, + {8446, L"ERROR_DS_DRA_OUT_OF_MEM"}, + {8447, L"ERROR_DS_DRA_MAIL_PROBLEM"}, + {8448, L"ERROR_DS_DRA_REF_ALREADY_EXISTS"}, + {8449, L"ERROR_DS_DRA_REF_NOT_FOUND"}, + {8450, L"ERROR_DS_DRA_OBJ_IS_REP_SOURCE"}, + {8451, L"ERROR_DS_DRA_DB_ERROR"}, + {8452, L"ERROR_DS_DRA_NO_REPLICA"}, + {8453, L"ERROR_DS_DRA_ACCESS_DENIED"}, + {8454, L"ERROR_DS_DRA_NOT_SUPPORTED"}, + {8455, L"ERROR_DS_DRA_RPC_CANCELLED"}, + {8456, L"ERROR_DS_DRA_SOURCE_DISABLED"}, + {8457, L"ERROR_DS_DRA_SINK_DISABLED"}, + {8458, L"ERROR_DS_DRA_NAME_COLLISION"}, + {8459, L"ERROR_DS_DRA_SOURCE_REINSTALLED"}, + {8460, L"ERROR_DS_DRA_MISSING_PARENT"}, + {8461, L"ERROR_DS_DRA_PREEMPTED"}, + {8462, L"ERROR_DS_DRA_ABANDON_SYNC"}, + {8463, L"ERROR_DS_DRA_SHUTDOWN"}, + {8464, L"ERROR_DS_DRA_INCOMPATIBLE_PARTIAL_SET"}, + {8465, L"ERROR_DS_DRA_SOURCE_IS_PARTIAL_REPLICA"}, + {8466, L"ERROR_DS_DRA_EXTN_CONNECTION_FAILED"}, + {8467, L"ERROR_DS_INSTALL_SCHEMA_MISMATCH"}, + {8468, L"ERROR_DS_DUP_LINK_ID"}, + {8469, L"ERROR_DS_NAME_ERROR_RESOLVING"}, + {8470, L"ERROR_DS_NAME_ERROR_NOT_FOUND"}, + {8471, L"ERROR_DS_NAME_ERROR_NOT_UNIQUE"}, + {8472, L"ERROR_DS_NAME_ERROR_NO_MAPPING"}, + {8473, L"ERROR_DS_NAME_ERROR_DOMAIN_ONLY"}, + {8474, L"ERROR_DS_NAME_ERROR_NO_SYNTACTICAL_MAPPING"}, + {8475, L"ERROR_DS_CONSTRUCTED_ATT_MOD"}, + {8476, L"ERROR_DS_WRONG_OM_OBJ_CLASS"}, + {8477, L"ERROR_DS_DRA_REPL_PENDING"}, + {8478, L"ERROR_DS_DS_REQUIRED"}, + {8479, L"ERROR_DS_INVALID_LDAP_DISPLAY_NAME"}, + {8480, L"ERROR_DS_NON_BASE_SEARCH"}, + {8481, L"ERROR_DS_CANT_RETRIEVE_ATTS"}, + {8482, L"ERROR_DS_BACKLINK_WITHOUT_LINK"}, + {8483, L"ERROR_DS_EPOCH_MISMATCH"}, + {8484, L"ERROR_DS_SRC_NAME_MISMATCH"}, + {8485, L"ERROR_DS_SRC_AND_DST_NC_IDENTICAL"}, + {8486, L"ERROR_DS_DST_NC_MISMATCH"}, + {8487, L"ERROR_DS_NOT_AUTHORITIVE_FOR_DST_NC"}, + {8488, L"ERROR_DS_SRC_GUID_MISMATCH"}, + {8489, L"ERROR_DS_CANT_MOVE_DELETED_OBJECT"}, + {8490, L"ERROR_DS_PDC_OPERATION_IN_PROGRESS"}, + {8491, L"ERROR_DS_CROSS_DOMAIN_CLEANUP_REQD"}, + {8492, L"ERROR_DS_ILLEGAL_XDOM_MOVE_OPERATION"}, + {8493, L"ERROR_DS_CANT_WITH_ACCT_GROUP_MEMBERSHPS"}, + {8494, L"ERROR_DS_NC_MUST_HAVE_NC_PARENT"}, + {8495, L"ERROR_DS_CR_IMPOSSIBLE_TO_VALIDATE"}, + {8496, L"ERROR_DS_DST_DOMAIN_NOT_NATIVE"}, + {8497, L"ERROR_DS_MISSING_INFRASTRUCTURE_CONTAINER"}, + {8498, L"ERROR_DS_CANT_MOVE_ACCOUNT_GROUP"}, + {8499, L"ERROR_DS_CANT_MOVE_RESOURCE_GROUP"}, + {8500, L"ERROR_DS_INVALID_SEARCH_FLAG"}, + {8501, L"ERROR_DS_NO_TREE_DELETE_ABOVE_NC"}, + {8502, L"ERROR_DS_COULDNT_LOCK_TREE_FOR_DELETE"}, + {8503, L"ERROR_DS_COULDNT_IDENTIFY_OBJECTS_FOR_TREE_DELETE"}, + {8504, L"ERROR_DS_SAM_INIT_FAILURE"}, + {8505, L"ERROR_DS_SENSITIVE_GROUP_VIOLATION"}, + {8506, L"ERROR_DS_CANT_MOD_PRIMARYGROUPID"}, + {8507, L"ERROR_DS_ILLEGAL_BASE_SCHEMA_MOD"}, + {8508, L"ERROR_DS_NONSAFE_SCHEMA_CHANGE"}, + {8509, L"ERROR_DS_SCHEMA_UPDATE_DISALLOWED"}, + {8510, L"ERROR_DS_CANT_CREATE_UNDER_SCHEMA"}, + {8511, L"ERROR_DS_INSTALL_NO_SRC_SCH_VERSION"}, + {8512, L"ERROR_DS_INSTALL_NO_SCH_VERSION_IN_INIFILE"}, + {8513, L"ERROR_DS_INVALID_GROUP_TYPE"}, + {8514, L"ERROR_DS_NO_NEST_GLOBALGROUP_IN_MIXEDDOMAIN"}, + {8515, L"ERROR_DS_NO_NEST_LOCALGROUP_IN_MIXEDDOMAIN"}, + {8516, L"ERROR_DS_GLOBAL_CANT_HAVE_LOCAL_MEMBER"}, + {8517, L"ERROR_DS_GLOBAL_CANT_HAVE_UNIVERSAL_MEMBER"}, + {8518, L"ERROR_DS_UNIVERSAL_CANT_HAVE_LOCAL_MEMBER"}, + {8519, L"ERROR_DS_GLOBAL_CANT_HAVE_CROSSDOMAIN_MEMBER"}, + {8520, L"ERROR_DS_LOCAL_CANT_HAVE_CROSSDOMAIN_LOCAL_MEMBER"}, + {8521, L"ERROR_DS_HAVE_PRIMARY_MEMBERS"}, + {8522, L"ERROR_DS_STRING_SD_CONVERSION_FAILED"}, + {8523, L"ERROR_DS_NAMING_MASTER_GC"}, + {8524, L"ERROR_DS_DNS_LOOKUP_FAILURE"}, + {8525, L"ERROR_DS_COULDNT_UPDATE_SPNS"}, + {8526, L"ERROR_DS_CANT_RETRIEVE_SD"}, + {8527, L"ERROR_DS_KEY_NOT_UNIQUE"}, + {8528, L"ERROR_DS_WRONG_LINKED_ATT_SYNTAX"}, + {8529, L"ERROR_DS_SAM_NEED_BOOTKEY_PASSWORD"}, + {8530, L"ERROR_DS_SAM_NEED_BOOTKEY_FLOPPY"}, + {8531, L"ERROR_DS_CANT_START"}, + {8532, L"ERROR_DS_INIT_FAILURE"}, + {8533, L"ERROR_DS_NO_PKT_PRIVACY_ON_CONNECTION"}, + {8534, L"ERROR_DS_SOURCE_DOMAIN_IN_FOREST"}, + {8535, L"ERROR_DS_DESTINATION_DOMAIN_NOT_IN_FOREST"}, + {8536, L"ERROR_DS_DESTINATION_AUDITING_NOT_ENABLED"}, + {8537, L"ERROR_DS_CANT_FIND_DC_FOR_SRC_DOMAIN"}, + {8538, L"ERROR_DS_SRC_OBJ_NOT_GROUP_OR_USER"}, + {8539, L"ERROR_DS_SRC_SID_EXISTS_IN_FOREST"}, + {8540, L"ERROR_DS_SRC_AND_DST_OBJECT_CLASS_MISMATCH"}, + {8541, L"ERROR_SAM_INIT_FAILURE"}, + {8542, L"ERROR_DS_DRA_SCHEMA_INFO_SHIP"}, + {8543, L"ERROR_DS_DRA_SCHEMA_CONFLICT"}, + {8544, L"ERROR_DS_DRA_EARLIER_SCHEMA_CONFLICT"}, + {8545, L"ERROR_DS_DRA_OBJ_NC_MISMATCH"}, + {8546, L"ERROR_DS_NC_STILL_HAS_DSAS"}, + {8547, L"ERROR_DS_GC_REQUIRED"}, + {8548, L"ERROR_DS_LOCAL_MEMBER_OF_LOCAL_ONLY"}, + {8549, L"ERROR_DS_NO_FPO_IN_UNIVERSAL_GROUPS"}, + {8550, L"ERROR_DS_CANT_ADD_TO_GC"}, + {8551, L"ERROR_DS_NO_CHECKPOINT_WITH_PDC"}, + {8552, L"ERROR_DS_SOURCE_AUDITING_NOT_ENABLED"}, + {8553, L"ERROR_DS_CANT_CREATE_IN_NONDOMAIN_NC"}, + {8554, L"ERROR_DS_INVALID_NAME_FOR_SPN"}, + {8555, L"ERROR_DS_FILTER_USES_CONTRUCTED_ATTRS"}, + {8556, L"ERROR_DS_UNICODEPWD_NOT_IN_QUOTES"}, + {8557, L"ERROR_DS_MACHINE_ACCOUNT_QUOTA_EXCEEDED"}, + {8558, L"ERROR_DS_MUST_BE_RUN_ON_DST_DC"}, + {8559, L"ERROR_DS_SRC_DC_MUST_BE_SP4_OR_GREATER"}, + {8560, L"ERROR_DS_CANT_TREE_DELETE_CRITICAL_OBJ"}, + {8561, L"ERROR_DS_INIT_FAILURE_CONSOLE"}, + {8562, L"ERROR_DS_SAM_INIT_FAILURE_CONSOLE"}, + {8563, L"ERROR_DS_FOREST_VERSION_TOO_HIGH"}, + {8564, L"ERROR_DS_DOMAIN_VERSION_TOO_HIGH"}, + {8565, L"ERROR_DS_FOREST_VERSION_TOO_LOW"}, + {8566, L"ERROR_DS_DOMAIN_VERSION_TOO_LOW"}, + {8567, L"ERROR_DS_INCOMPATIBLE_VERSION"}, + {8568, L"ERROR_DS_LOW_DSA_VERSION"}, + {8569, L"ERROR_DS_NO_BEHAVIOR_VERSION_IN_MIXEDDOMAIN"}, + {8570, L"ERROR_DS_NOT_SUPPORTED_SORT_ORDER"}, + {8571, L"ERROR_DS_NAME_NOT_UNIQUE"}, + {8572, L"ERROR_DS_MACHINE_ACCOUNT_CREATED_PRENT4"}, + {8573, L"ERROR_DS_OUT_OF_VERSION_STORE"}, + {8574, L"ERROR_DS_INCOMPATIBLE_CONTROLS_USED"}, + {8575, L"ERROR_DS_NO_REF_DOMAIN"}, + {8576, L"ERROR_DS_RESERVED_LINK_ID"}, + {8577, L"ERROR_DS_LINK_ID_NOT_AVAILABLE"}, + {8578, L"ERROR_DS_AG_CANT_HAVE_UNIVERSAL_MEMBER"}, + {8579, L"ERROR_DS_MODIFYDN_DISALLOWED_BY_INSTANCE_TYPE"}, + {8580, L"ERROR_DS_NO_OBJECT_MOVE_IN_SCHEMA_NC"}, + {8581, L"ERROR_DS_MODIFYDN_DISALLOWED_BY_FLAG"}, + {8582, L"ERROR_DS_MODIFYDN_WRONG_GRANDPARENT"}, + {8583, L"ERROR_DS_NAME_ERROR_TRUST_REFERRAL"}, + {8584, L"ERROR_NOT_SUPPORTED_ON_STANDARD_SERVER"}, + {8585, L"ERROR_DS_CANT_ACCESS_REMOTE_PART_OF_AD"}, + {8586, L"ERROR_DS_CR_IMPOSSIBLE_TO_VALIDATE_V2"}, + {8587, L"ERROR_DS_THREAD_LIMIT_EXCEEDED"}, + {8588, L"ERROR_DS_NOT_CLOSEST"}, + {8589, L"ERROR_DS_CANT_DERIVE_SPN_WITHOUT_SERVER_REF"}, + {8590, L"ERROR_DS_SINGLE_USER_MODE_FAILED"}, + {8591, L"ERROR_DS_NTDSCRIPT_SYNTAX_ERROR"}, + {8592, L"ERROR_DS_NTDSCRIPT_PROCESS_ERROR"}, + {8593, L"ERROR_DS_DIFFERENT_REPL_EPOCHS"}, + {8594, L"ERROR_DS_DRS_EXTENSIONS_CHANGED"}, + {8595, L"ERROR_DS_REPLICA_SET_CHANGE_NOT_ALLOWED_ON_DISABLED_CR"}, + {8596, L"ERROR_DS_NO_MSDS_INTID"}, + {8597, L"ERROR_DS_DUP_MSDS_INTID"}, + {8598, L"ERROR_DS_EXISTS_IN_RDNATTID"}, + {8599, L"ERROR_DS_AUTHORIZATION_FAILED"}, + {8600, L"ERROR_DS_INVALID_SCRIPT"}, + {8601, L"ERROR_DS_REMOTE_CROSSREF_OP_FAILED"}, + {8602, L"ERROR_DS_CROSS_REF_BUSY"}, + {8603, L"ERROR_DS_CANT_DERIVE_SPN_FOR_DELETED_DOMAIN"}, + {8604, L"ERROR_DS_CANT_DEMOTE_WITH_WRITEABLE_NC"}, + {8605, L"ERROR_DS_DUPLICATE_ID_FOUND"}, + {8606, L"ERROR_DS_INSUFFICIENT_ATTR_TO_CREATE_OBJECT"}, + {8607, L"ERROR_DS_GROUP_CONVERSION_ERROR"}, + {8608, L"ERROR_DS_CANT_MOVE_APP_BASIC_GROUP"}, + {8609, L"ERROR_DS_CANT_MOVE_APP_QUERY_GROUP"}, + {8610, L"ERROR_DS_ROLE_NOT_VERIFIED"}, + {8611, L"ERROR_DS_WKO_CONTAINER_CANNOT_BE_SPECIAL"}, + {8612, L"ERROR_DS_DOMAIN_RENAME_IN_PROGRESS"}, + {8613, L"ERROR_DS_EXISTING_AD_CHILD_NC"}, + {8614, L"ERROR_DS_REPL_LIFETIME_EXCEEDED"}, + {8615, L"ERROR_DS_DISALLOWED_IN_SYSTEM_CONTAINER"}, + {8616, L"ERROR_DS_LDAP_SEND_QUEUE_FULL"}, + {8617, L"ERROR_DS_DRA_OUT_SCHEDULE_WINDOW"}, + {8618, L"ERROR_DS_POLICY_NOT_KNOWN"}, + {8619, L"ERROR_NO_SITE_SETTINGS_OBJECT"}, + {8620, L"ERROR_NO_SECRETS"}, + {8621, L"ERROR_NO_WRITABLE_DC_FOUND"}, + {8622, L"ERROR_DS_NO_SERVER_OBJECT"}, + {8623, L"ERROR_DS_NO_NTDSA_OBJECT"}, + {8624, L"ERROR_DS_NON_ASQ_SEARCH"}, + {8625, L"ERROR_DS_AUDIT_FAILURE"}, + {8626, L"ERROR_DS_INVALID_SEARCH_FLAG_SUBTREE"}, + {8627, L"ERROR_DS_INVALID_SEARCH_FLAG_TUPLE"}, + {8628, L"ERROR_DS_HIERARCHY_TABLE_TOO_DEEP"}, + {8629, L"ERROR_DS_DRA_CORRUPT_UTD_VECTOR"}, + {8630, L"ERROR_DS_DRA_SECRETS_DENIED"}, + {8631, L"ERROR_DS_RESERVED_MAPI_ID"}, + {8632, L"ERROR_DS_MAPI_ID_NOT_AVAILABLE"}, + {8633, L"ERROR_DS_DRA_MISSING_KRBTGT_SECRET"}, + {8634, L"ERROR_DS_DOMAIN_NAME_EXISTS_IN_FOREST"}, + {8635, L"ERROR_DS_FLAT_NAME_EXISTS_IN_FOREST"}, + {8636, L"ERROR_INVALID_USER_PRINCIPAL_NAME"}, + {8637, L"ERROR_DS_OID_MAPPED_GROUP_CANT_HAVE_MEMBERS"}, + {8638, L"ERROR_DS_OID_NOT_FOUND"}, + {8639, L"ERROR_DS_DRA_RECYCLED_TARGET"}, + {8640, L"ERROR_DS_DISALLOWED_NC_REDIRECT"}, + {8641, L"ERROR_DS_HIGH_ADLDS_FFL"}, + {8642, L"ERROR_DS_HIGH_DSA_VERSION"}, + {8643, L"ERROR_DS_LOW_ADLDS_FFL"}, + {8644, L"ERROR_DOMAIN_SID_SAME_AS_LOCAL_WORKSTATION"}, + {8645, L"ERROR_DS_UNDELETE_SAM_VALIDATION_FAILED"}, + {8646, L"ERROR_INCORRECT_ACCOUNT_TYPE"}, + {9001, L"DNS_ERROR_RCODE_FORMAT_ERROR"}, + {9002, L"DNS_ERROR_RCODE_SERVER_FAILURE"}, + {9003, L"DNS_ERROR_RCODE_NAME_ERROR"}, + {9004, L"DNS_ERROR_RCODE_NOT_IMPLEMENTED"}, + {9005, L"DNS_ERROR_RCODE_REFUSED"}, + {9006, L"DNS_ERROR_RCODE_YXDOMAIN"}, + {9007, L"DNS_ERROR_RCODE_YXRRSET"}, + {9008, L"DNS_ERROR_RCODE_NXRRSET"}, + {9009, L"DNS_ERROR_RCODE_NOTAUTH"}, + {9010, L"DNS_ERROR_RCODE_NOTZONE"}, + {9016, L"DNS_ERROR_RCODE_BADSIG"}, + {9017, L"DNS_ERROR_RCODE_BADKEY"}, + {9018, L"DNS_ERROR_RCODE_BADTIME"}, + {9101, L"DNS_ERROR_KEYMASTER_REQUIRED"}, + {9102, L"DNS_ERROR_NOT_ALLOWED_ON_SIGNED_ZONE"}, + {9103, L"DNS_ERROR_NSEC3_INCOMPATIBLE_WITH_RSA_SHA1"}, + {9104, L"DNS_ERROR_NOT_ENOUGH_SIGNING_KEY_DESCRIPTORS"}, + {9105, L"DNS_ERROR_UNSUPPORTED_ALGORITHM"}, + {9106, L"DNS_ERROR_INVALID_KEY_SIZE"}, + {9107, L"DNS_ERROR_SIGNING_KEY_NOT_ACCESSIBLE"}, + {9108, L"DNS_ERROR_KSP_DOES_NOT_SUPPORT_PROTECTION"}, + {9109, L"DNS_ERROR_UNEXPECTED_DATA_PROTECTION_ERROR"}, + {9110, L"DNS_ERROR_UNEXPECTED_CNG_ERROR"}, + {9111, L"DNS_ERROR_UNKNOWN_SIGNING_PARAMETER_VERSION"}, + {9112, L"DNS_ERROR_KSP_NOT_ACCESSIBLE"}, + {9113, L"DNS_ERROR_TOO_MANY_SKDS"}, + {9114, L"DNS_ERROR_INVALID_ROLLOVER_PERIOD"}, + {9115, L"DNS_ERROR_INVALID_INITIAL_ROLLOVER_OFFSET"}, + {9116, L"DNS_ERROR_ROLLOVER_IN_PROGRESS"}, + {9117, L"DNS_ERROR_STANDBY_KEY_NOT_PRESENT"}, + {9118, L"DNS_ERROR_NOT_ALLOWED_ON_ZSK"}, + {9119, L"DNS_ERROR_NOT_ALLOWED_ON_ACTIVE_SKD"}, + {9120, L"DNS_ERROR_ROLLOVER_ALREADY_QUEUED"}, + {9121, L"DNS_ERROR_NOT_ALLOWED_ON_UNSIGNED_ZONE"}, + {9122, L"DNS_ERROR_BAD_KEYMASTER"}, + {9123, L"DNS_ERROR_INVALID_SIGNATURE_VALIDITY_PERIOD"}, + {9124, L"DNS_ERROR_INVALID_NSEC3_ITERATION_COUNT"}, + {9125, L"DNS_ERROR_DNSSEC_IS_DISABLED"}, + {9126, L"DNS_ERROR_INVALID_XML"}, + {9127, L"DNS_ERROR_NO_VALID_TRUST_ANCHORS"}, + {9128, L"DNS_ERROR_ROLLOVER_NOT_POKEABLE"}, + {9129, L"DNS_ERROR_NSEC3_NAME_COLLISION"}, + {9130, L"DNS_ERROR_NSEC_INCOMPATIBLE_WITH_NSEC3_RSA_SHA1"}, + {9501, L"DNS_INFO_NO_RECORDS"}, + {9502, L"DNS_ERROR_BAD_PACKET"}, + {9503, L"DNS_ERROR_NO_PACKET"}, + {9504, L"DNS_ERROR_RCODE"}, + {9505, L"DNS_ERROR_UNSECURE_PACKET"}, + {9506, L"DNS_REQUEST_PENDING"}, + {9551, L"DNS_ERROR_INVALID_TYPE"}, + {9552, L"DNS_ERROR_INVALID_IP_ADDRESS"}, + {9553, L"DNS_ERROR_INVALID_PROPERTY"}, + {9554, L"DNS_ERROR_TRY_AGAIN_LATER"}, + {9555, L"DNS_ERROR_NOT_UNIQUE"}, + {9556, L"DNS_ERROR_NON_RFC_NAME"}, + {9557, L"DNS_STATUS_FQDN"}, + {9558, L"DNS_STATUS_DOTTED_NAME"}, + {9559, L"DNS_STATUS_SINGLE_PART_NAME"}, + {9560, L"DNS_ERROR_INVALID_NAME_CHAR"}, + {9561, L"DNS_ERROR_NUMERIC_NAME"}, + {9562, L"DNS_ERROR_NOT_ALLOWED_ON_ROOT_SERVER"}, + {9563, L"DNS_ERROR_NOT_ALLOWED_UNDER_DELEGATION"}, + {9564, L"DNS_ERROR_CANNOT_FIND_ROOT_HINTS"}, + {9565, L"DNS_ERROR_INCONSISTENT_ROOT_HINTS"}, + {9566, L"DNS_ERROR_DWORD_VALUE_TOO_SMALL"}, + {9567, L"DNS_ERROR_DWORD_VALUE_TOO_LARGE"}, + {9568, L"DNS_ERROR_BACKGROUND_LOADING"}, + {9569, L"DNS_ERROR_NOT_ALLOWED_ON_RODC"}, + {9570, L"DNS_ERROR_NOT_ALLOWED_UNDER_DNAME"}, + {9571, L"DNS_ERROR_DELEGATION_REQUIRED"}, + {9572, L"DNS_ERROR_INVALID_POLICY_TABLE"}, + {9601, L"DNS_ERROR_ZONE_DOES_NOT_EXIST"}, + {9602, L"DNS_ERROR_NO_ZONE_INFO"}, + {9603, L"DNS_ERROR_INVALID_ZONE_OPERATION"}, + {9604, L"DNS_ERROR_ZONE_CONFIGURATION_ERROR"}, + {9605, L"DNS_ERROR_ZONE_HAS_NO_SOA_RECORD"}, + {9606, L"DNS_ERROR_ZONE_HAS_NO_NS_RECORDS"}, + {9607, L"DNS_ERROR_ZONE_LOCKED"}, + {9608, L"DNS_ERROR_ZONE_CREATION_FAILED"}, + {9609, L"DNS_ERROR_ZONE_ALREADY_EXISTS"}, + {9610, L"DNS_ERROR_AUTOZONE_ALREADY_EXISTS"}, + {9611, L"DNS_ERROR_INVALID_ZONE_TYPE"}, + {9612, L"DNS_ERROR_SECONDARY_REQUIRES_MASTER_IP"}, + {9613, L"DNS_ERROR_ZONE_NOT_SECONDARY"}, + {9614, L"DNS_ERROR_NEED_SECONDARY_ADDRESSES"}, + {9615, L"DNS_ERROR_WINS_INIT_FAILED"}, + {9616, L"DNS_ERROR_NEED_WINS_SERVERS"}, + {9617, L"DNS_ERROR_NBSTAT_INIT_FAILED"}, + {9618, L"DNS_ERROR_SOA_DELETE_INVALID"}, + {9619, L"DNS_ERROR_FORWARDER_ALREADY_EXISTS"}, + {9620, L"DNS_ERROR_ZONE_REQUIRES_MASTER_IP"}, + {9621, L"DNS_ERROR_ZONE_IS_SHUTDOWN"}, + {9622, L"DNS_ERROR_ZONE_LOCKED_FOR_SIGNING"}, + {9651, L"DNS_ERROR_PRIMARY_REQUIRES_DATAFILE"}, + {9652, L"DNS_ERROR_INVALID_DATAFILE_NAME"}, + {9653, L"DNS_ERROR_DATAFILE_OPEN_FAILURE"}, + {9654, L"DNS_ERROR_FILE_WRITEBACK_FAILED"}, + {9655, L"DNS_ERROR_DATAFILE_PARSING"}, + {9701, L"DNS_ERROR_RECORD_DOES_NOT_EXIST"}, + {9702, L"DNS_ERROR_RECORD_FORMAT"}, + {9703, L"DNS_ERROR_NODE_CREATION_FAILED"}, + {9704, L"DNS_ERROR_UNKNOWN_RECORD_TYPE"}, + {9705, L"DNS_ERROR_RECORD_TIMED_OUT"}, + {9706, L"DNS_ERROR_NAME_NOT_IN_ZONE"}, + {9707, L"DNS_ERROR_CNAME_LOOP"}, + {9708, L"DNS_ERROR_NODE_IS_CNAME"}, + {9709, L"DNS_ERROR_CNAME_COLLISION"}, + {9710, L"DNS_ERROR_RECORD_ONLY_AT_ZONE_ROOT"}, + {9711, L"DNS_ERROR_RECORD_ALREADY_EXISTS"}, + {9712, L"DNS_ERROR_SECONDARY_DATA"}, + {9713, L"DNS_ERROR_NO_CREATE_CACHE_DATA"}, + {9714, L"DNS_ERROR_NAME_DOES_NOT_EXIST"}, + {9715, L"DNS_WARNING_PTR_CREATE_FAILED"}, + {9716, L"DNS_WARNING_DOMAIN_UNDELETED"}, + {9717, L"DNS_ERROR_DS_UNAVAILABLE"}, + {9718, L"DNS_ERROR_DS_ZONE_ALREADY_EXISTS"}, + {9719, L"DNS_ERROR_NO_BOOTFILE_IF_DS_ZONE"}, + {9720, L"DNS_ERROR_NODE_IS_DNAME"}, + {9721, L"DNS_ERROR_DNAME_COLLISION"}, + {9722, L"DNS_ERROR_ALIAS_LOOP"}, + {9751, L"DNS_INFO_AXFR_COMPLETE"}, + {9752, L"DNS_ERROR_AXFR"}, + {9753, L"DNS_INFO_ADDED_LOCAL_WINS"}, + {9801, L"DNS_STATUS_CONTINUE_NEEDED"}, + {9851, L"DNS_ERROR_NO_TCPIP"}, + {9852, L"DNS_ERROR_NO_DNS_SERVERS"}, + {9901, L"DNS_ERROR_DP_DOES_NOT_EXIST"}, + {9902, L"DNS_ERROR_DP_ALREADY_EXISTS"}, + {9903, L"DNS_ERROR_DP_NOT_ENLISTED"}, + {9904, L"DNS_ERROR_DP_ALREADY_ENLISTED"}, + {9905, L"DNS_ERROR_DP_NOT_AVAILABLE"}, + {9906, L"DNS_ERROR_DP_FSMO_ERROR"}, + {10004, L"WSAEINTR"}, + {10009, L"WSAEBADF"}, + {10013, L"WSAEACCES"}, + {10014, L"WSAEFAULT"}, + {10022, L"WSAEINVAL"}, + {10024, L"WSAEMFILE"}, + {10035, L"WSAEWOULDBLOCK"}, + {10036, L"WSAEINPROGRESS"}, + {10037, L"WSAEALREADY"}, + {10038, L"WSAENOTSOCK"}, + {10039, L"WSAEDESTADDRREQ"}, + {10040, L"WSAEMSGSIZE"}, + {10041, L"WSAEPROTOTYPE"}, + {10042, L"WSAENOPROTOOPT"}, + {10043, L"WSAEPROTONOSUPPORT"}, + {10044, L"WSAESOCKTNOSUPPORT"}, + {10045, L"WSAEOPNOTSUPP"}, + {10046, L"WSAEPFNOSUPPORT"}, + {10047, L"WSAEAFNOSUPPORT"}, + {10048, L"WSAEADDRINUSE"}, + {10049, L"WSAEADDRNOTAVAIL"}, + {10050, L"WSAENETDOWN"}, + {10051, L"WSAENETUNREACH"}, + {10052, L"WSAENETRESET"}, + {10053, L"WSAECONNABORTED"}, + {10054, L"WSAECONNRESET"}, + {10055, L"WSAENOBUFS"}, + {10056, L"WSAEISCONN"}, + {10057, L"WSAENOTCONN"}, + {10058, L"WSAESHUTDOWN"}, + {10059, L"WSAETOOMANYREFS"}, + {10060, L"WSAETIMEDOUT"}, + {10061, L"WSAECONNREFUSED"}, + {10062, L"WSAELOOP"}, + {10063, L"WSAENAMETOOLONG"}, + {10064, L"WSAEHOSTDOWN"}, + {10065, L"WSAEHOSTUNREACH"}, + {10066, L"WSAENOTEMPTY"}, + {10067, L"WSAEPROCLIM"}, + {10068, L"WSAEUSERS"}, + {10069, L"WSAEDQUOT"}, + {10070, L"WSAESTALE"}, + {10071, L"WSAEREMOTE"}, + {10091, L"WSASYSNOTREADY"}, + {10092, L"WSAVERNOTSUPPORTED"}, + {10093, L"WSANOTINITIALISED"}, + {10101, L"WSAEDISCON"}, + {10102, L"WSAENOMORE"}, + {10103, L"WSAECANCELLED"}, + {10104, L"WSAEINVALIDPROCTABLE"}, + {10105, L"WSAEINVALIDPROVIDER"}, + {10106, L"WSAEPROVIDERFAILEDINIT"}, + {10107, L"WSASYSCALLFAILURE"}, + {10108, L"WSASERVICE_NOT_FOUND"}, + {10109, L"WSATYPE_NOT_FOUND"}, + {10110, L"WSA_E_NO_MORE"}, + {10111, L"WSA_E_CANCELLED"}, + {10112, L"WSAEREFUSED"}, + {11001, L"WSAHOST_NOT_FOUND"}, + {11002, L"WSATRY_AGAIN"}, + {11003, L"WSANO_RECOVERY"}, + {11004, L"WSANO_DATA"}, + {11005, L"WSA_QOS_RECEIVERS"}, + {11006, L"WSA_QOS_SENDERS"}, + {11007, L"WSA_QOS_NO_SENDERS"}, + {11008, L"WSA_QOS_NO_RECEIVERS"}, + {11009, L"WSA_QOS_REQUEST_CONFIRMED"}, + {11010, L"WSA_QOS_ADMISSION_FAILURE"}, + {11011, L"WSA_QOS_POLICY_FAILURE"}, + {11012, L"WSA_QOS_BAD_STYLE"}, + {11013, L"WSA_QOS_BAD_OBJECT"}, + {11014, L"WSA_QOS_TRAFFIC_CTRL_ERROR"}, + {11015, L"WSA_QOS_GENERIC_ERROR"}, + {11016, L"WSA_QOS_ESERVICETYPE"}, + {11017, L"WSA_QOS_EFLOWSPEC"}, + {11018, L"WSA_QOS_EPROVSPECBUF"}, + {11019, L"WSA_QOS_EFILTERSTYLE"}, + {11020, L"WSA_QOS_EFILTERTYPE"}, + {11021, L"WSA_QOS_EFILTERCOUNT"}, + {11022, L"WSA_QOS_EOBJLENGTH"}, + {11023, L"WSA_QOS_EFLOWCOUNT"}, + {11024, L"WSA_QOS_EUNKOWNPSOBJ"}, + {11025, L"WSA_QOS_EPOLICYOBJ"}, + {11026, L"WSA_QOS_EFLOWDESC"}, + {11027, L"WSA_QOS_EPSFLOWSPEC"}, + {11028, L"WSA_QOS_EPSFILTERSPEC"}, + {11029, L"WSA_QOS_ESDMODEOBJ"}, + {11030, L"WSA_QOS_ESHAPERATEOBJ"}, + {11031, L"WSA_QOS_RESERVED_PETYPE"}, + {11032, L"WSA_SECURE_HOST_NOT_FOUND"}, + {11033, L"WSA_IPSEC_NAME_POLICY_ERROR"}}; + +const wchar_t* errorCodeName(DWORD code) +{ + static const wchar_t* NotFound = L"unknown error"; + + auto itor = std::lower_bound(std::begin(g_codes), std::end(g_codes), CodeName{code}, + [](auto&& a, auto&& b) { + return (a.code < b.code); + }); + + if (itor != std::end(g_codes)) { + return itor->name; + } + + return NotFound; +} + +} // namespace MOBase diff --git a/libs/uibase/src/eventfilter.cpp b/libs/uibase/src/eventfilter.cpp new file mode 100644 index 0000000..3b624c7 --- /dev/null +++ b/libs/uibase/src/eventfilter.cpp @@ -0,0 +1,34 @@ +/* +Copyright (C) 2016 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include + +namespace MOBase +{ + +EventFilter::EventFilter(QObject* parent, const EventFilter::HandlerFunc& handler) + : QObject(parent), m_Handler(handler) +{} + +bool EventFilter::eventFilter(QObject* obj, QEvent* event) +{ + return m_Handler(obj, event); +} + +} // namespace MOBase diff --git a/libs/uibase/src/executableinfo.cpp b/libs/uibase/src/executableinfo.cpp new file mode 100644 index 0000000..1573a0b --- /dev/null +++ b/libs/uibase/src/executableinfo.cpp @@ -0,0 +1,105 @@ +#include + +using namespace MOBase; + +ExecutableForcedLoadSetting::ExecutableForcedLoadSetting(const QString& process, + const QString& library) + : m_Enabled(false), m_Process(process), m_Library(library), m_Forced(false) +{} + +ExecutableForcedLoadSetting& ExecutableForcedLoadSetting::withEnabled(bool enabled) +{ + m_Enabled = enabled; + return *this; +} + +ExecutableForcedLoadSetting& ExecutableForcedLoadSetting::withForced(bool forced) +{ + m_Forced = forced; + return *this; +} + +bool ExecutableForcedLoadSetting::enabled() const +{ + return m_Enabled; +} + +bool ExecutableForcedLoadSetting::forced() const +{ + return m_Forced; +} + +QString ExecutableForcedLoadSetting::library() const +{ + return m_Library; +} + +QString ExecutableForcedLoadSetting::process() const +{ + return m_Process; +} + +MOBase::ExecutableInfo::ExecutableInfo(const QString& title, const QFileInfo& binary) + : m_Title(title), m_Binary(binary), + m_WorkingDirectory(binary.exists() ? binary.absoluteDir() : QString()), + m_SteamAppID() +{} + +ExecutableInfo& ExecutableInfo::withArgument(const QString& argument) +{ + m_Arguments.append(argument); + return *this; +} + +ExecutableInfo& ExecutableInfo::withWorkingDirectory(const QDir& workingDirectory) +{ + m_WorkingDirectory = workingDirectory; + return *this; +} + +ExecutableInfo& MOBase::ExecutableInfo::withSteamAppId(const QString& appId) +{ + m_SteamAppID = appId; + return *this; +} + +ExecutableInfo& ExecutableInfo::asCustom() +{ + m_Custom = true; + return *this; +} + +bool ExecutableInfo::isValid() const +{ + return m_Binary.exists(); +} + +QString ExecutableInfo::title() const +{ + return m_Title; +} + +QFileInfo ExecutableInfo::binary() const +{ + return m_Binary; +} + +QStringList ExecutableInfo::arguments() const +{ + return m_Arguments; +} + +QDir ExecutableInfo::workingDirectory() const +{ + return m_WorkingDirectory; +} + +QString ExecutableInfo::steamAppID() const +{ + return m_SteamAppID; +} + +bool ExecutableInfo::isCustom() const +{ + return m_Custom; +} diff --git a/libs/uibase/src/expanderwidget.cpp b/libs/uibase/src/expanderwidget.cpp new file mode 100644 index 0000000..451ab07 --- /dev/null +++ b/libs/uibase/src/expanderwidget.cpp @@ -0,0 +1,93 @@ +#include +#include + +namespace MOBase +{ + +ExpanderWidget::ExpanderWidget() : m_button(nullptr), m_content(nullptr), opened_(false) +{} + +ExpanderWidget::ExpanderWidget(QToolButton* button, QWidget* content) : ExpanderWidget() +{ + set(button, content); +} + +void ExpanderWidget::set(QToolButton* button, QWidget* content, bool o) +{ + m_button = button; + m_content = content; + + m_button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + QObject::connect(m_button, &QToolButton::clicked, [&] { + toggle(); + }); + + toggle(o); +} + +void ExpanderWidget::toggle() +{ + if (opened()) { + toggle(false); + } else { + toggle(true); + } +} + +void ExpanderWidget::toggle(bool b) +{ + if (b != opened_) { + emit aboutToToggle(b); + } + + if (b) { + m_button->setArrowType(Qt::DownArrow); + m_content->show(); + } else { + m_button->setArrowType(Qt::RightArrow); + m_content->hide(); + } + + if (b != opened_) { + // the state has to be remembered instead of using m_content's visibility + // because saving the state in saveConflictExpandersState() happens after the + // dialog is closed, which marks all the widgets hidden + opened_ = b; + + emit toggled(b); + } +} + +bool ExpanderWidget::opened() const +{ + return opened_; +} + +QByteArray ExpanderWidget::saveState() const +{ + QByteArray result; + QDataStream stream(&result, QIODevice::WriteOnly); + + stream << opened(); + + return result; +} + +void ExpanderWidget::restoreState(const QByteArray& a) +{ + QDataStream stream(a); + + bool opened = false; + stream >> opened; + + if (stream.status() == QDataStream::Ok) { + toggle(opened); + } +} + +QToolButton* ExpanderWidget::button() const +{ + return m_button; +} + +} // namespace MOBase diff --git a/libs/uibase/src/filesystemutilities.cpp b/libs/uibase/src/filesystemutilities.cpp new file mode 100644 index 0000000..6a01324 --- /dev/null +++ b/libs/uibase/src/filesystemutilities.cpp @@ -0,0 +1,68 @@ +#include + +#include +#include + +namespace MOBase +{ + +bool fixDirectoryName(QString& name) +{ + QString temp = name.simplified(); + while (temp.endsWith('.')) + temp.chop(1); + + temp.replace(QRegularExpression(R"([<>:"/\\|?*])"), ""); + static QString invalidNames[] = {"CON", "PRN", "AUX", "NUL", "COM1", "COM2", + "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", + "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", + "LPT6", "LPT7", "LPT8", "LPT9"}; + for (unsigned int i = 0; i < sizeof(invalidNames) / sizeof(QString); ++i) { + if (temp == invalidNames[i]) { + temp = ""; + break; + } + } + + temp = temp.simplified(); + + if (temp.length() >= 1) { + name = temp; + return true; + } else { + return false; + } +} + +QString sanitizeFileName(const QString& name, const QString& replacement) +{ + QString new_name = name; + + // Remove characters not allowed by Windows + new_name.replace(QRegularExpression("[\\x{00}-\\x{1f}\\\\/:\\*\\?\"<>|]"), + replacement); + + // Don't end with a period or a space + // Don't be "." or ".." + new_name.remove(QRegularExpression("[\\. ]*$")); + + // Recurse until stuff stops changing + if (new_name != name) { + return sanitizeFileName(new_name); + } + return new_name; +} + +bool validFileName(const QString& name) +{ + if (name.isEmpty()) { + return false; + } + if (name == "." || name == "..") { + return false; + } + + return (name == sanitizeFileName(name)); +} + +} // namespace MOBase diff --git a/libs/uibase/src/filterwidget.cpp b/libs/uibase/src/filterwidget.cpp new file mode 100644 index 0000000..cb0c503 --- /dev/null +++ b/libs/uibase/src/filterwidget.cpp @@ -0,0 +1,693 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MOBase +{ + +void setStyleProperty(QWidget* w, const char* k, const QVariant& v) +{ + w->setProperty(k, v); + + if (auto* s = w->style()) { + // changing properties doesn't repolish automatically + s->unpolish(w); + s->polish(w); + } + + // the qss will probably change the border, which requires sending a manual + // StyleChange because the box model has changed + QEvent event(QEvent::StyleChange); + QApplication::sendEvent(w, &event); + w->update(); + w->updateGeometry(); +} + +FilterWidgetProxyModel::FilterWidgetProxyModel(FilterWidget& fw, QWidget* parent) + : QSortFilterProxyModel(parent), m_filter(fw) +{ + setRecursiveFilteringEnabled(true); +} + +bool FilterWidgetProxyModel::filterAcceptsRow(int sourceRow, + const QModelIndex& sourceParent) const +{ + if (!m_filter.filteringEnabled()) { + return true; + } + + const auto cols = sourceModel()->columnCount(); + + const auto m = m_filter.matches([&](auto&& regex) { + if (m_filter.filterColumn() == -1) { + for (int c = 0; c < cols; ++c) { + if (columnMatches(sourceRow, sourceParent, c, regex)) { + return true; + } + } + + return false; + } else { + return columnMatches(sourceRow, sourceParent, m_filter.filterColumn(), regex); + } + }); + + return m; +} + +bool FilterWidgetProxyModel::columnMatches(int sourceRow, + const QModelIndex& sourceParent, int c, + const QRegularExpression& regex) const +{ + QModelIndex index = sourceModel()->index(sourceRow, c, sourceParent); + const auto text = sourceModel()->data(index, Qt::DisplayRole).toString(); + + return regex.match(text).hasMatch(); +} + +void FilterWidgetProxyModel::sort(int column, Qt::SortOrder order) +{ + if (m_filter.useSourceSort()) { + // without this, the proxy seems to sometimes ignore the sorting done below, + // no idea why + QSortFilterProxyModel::sort(-1, order); + + sourceModel()->sort(column, order); + } else { + QSortFilterProxyModel::sort(column, order); + } +} + +bool FilterWidgetProxyModel::lessThan(const QModelIndex& left, + const QModelIndex& right) const +{ + if (auto lt = m_filter.sortPredicate()) { + return lt(left, right); + } else { + return QSortFilterProxyModel::lessThan(left, right); + } +} + +static FilterWidget::Options s_options; + +FilterWidget::FilterWidget() + : m_edit(nullptr), m_list(nullptr), m_proxy(nullptr), m_eventFilter(nullptr), + m_clear(nullptr), m_timer(nullptr), m_useDelay(false), m_valid(true), + m_useSourceSort(false), m_filterColumn(-1), m_filteringEnabled(true), + m_filteredBorder(true) +{ + m_timer = new QTimer(this); + m_timer->setSingleShot(true); + QObject::connect( + m_timer, &QTimer::timeout, this, + [&] { + set(); + }, + Qt::QueuedConnection); +} + +void FilterWidget::setOptions(const Options& o) +{ + s_options = o; +} + +FilterWidget::Options FilterWidget::options() +{ + return s_options; +} + +void FilterWidget::setEdit(QLineEdit* edit) +{ + unhookEdit(); + + m_edit = edit; + + if (m_edit) { + m_edit->setPlaceholderText(QObject::tr("Filter")); + + createClear(); + hookEdit(); + clear(); + } +} + +void FilterWidget::setList(QAbstractItemView* list) +{ + unhookList(); + + m_list = list; + + if (list) { + hookList(); + } else { + m_proxy = nullptr; + } +} + +void FilterWidget::clear() +{ + if (!m_edit) { + return; + } + + m_edit->clear(); +} + +void FilterWidget::scrollToSelection() +{ + if (options().scrollToSelection && m_list && + m_list->selectionModel()->hasSelection()) { + m_list->scrollTo(m_list->selectionModel()->selectedIndexes()[0]); + } +} + +bool FilterWidget::empty() const +{ + return m_text.isEmpty(); +} + +void FilterWidget::setUpdateDelay(bool b) +{ + m_useDelay = b; +} + +bool FilterWidget::hasUpdateDelay() const +{ + return m_useDelay; +} + +void FilterWidget::setUseSourceSort(bool b) +{ + m_useSourceSort = b; +} + +bool FilterWidget::useSourceSort() const +{ + return m_useSourceSort; +} + +void FilterWidget::setSortPredicate(sortFun f) +{ + m_lt = f; +} + +const FilterWidget::sortFun& FilterWidget::sortPredicate() const +{ + return m_lt; +} + +void FilterWidget::setFilterColumn(int i) +{ + m_filterColumn = i; +} + +int FilterWidget::filterColumn() const +{ + return m_filterColumn; +} + +void FilterWidget::setFilteringEnabled(bool b) +{ + m_filteringEnabled = b; +} + +bool FilterWidget::filteringEnabled() const +{ + return m_filteringEnabled; +} + +void FilterWidget::setFilteredBorder(bool b) +{ + m_filteredBorder = b; +} + +bool FilterWidget::filteredBorder() const +{ + return m_filteredBorder; +} + +FilterWidgetProxyModel* FilterWidget::proxyModel() +{ + return m_proxy; +} + +QAbstractItemModel* FilterWidget::sourceModel() +{ + if (m_proxy) { + return m_proxy->sourceModel(); + } else if (m_list) { + return m_list->model(); + } else { + return nullptr; + } +} + +QModelIndex FilterWidget::mapFromSource(const QModelIndex& index) const +{ + if (m_proxy) { + return m_proxy->mapFromSource(index); + } else { + log::error("FilterWidget::mapFromSource() called, but proxy isn't set up"); + return index; + } +} + +QModelIndex FilterWidget::mapToSource(const QModelIndex& index) const +{ + if (m_proxy) { + return m_proxy->mapToSource(index); + } else { + log::error("FilterWidget::mapToSource() called, but proxy isn't set up"); + return index; + } +} + +QItemSelection FilterWidget::mapSelectionFromSource(const QItemSelection& sel) const +{ + if (m_proxy) { + return m_proxy->mapSelectionFromSource(sel); + } else { + log::error("FilterWidget::mapToSource() called, but proxy isn't set up"); + return sel; + } +} + +QItemSelection FilterWidget::mapSelectionToSource(const QItemSelection& sel) const +{ + if (m_proxy) { + return m_proxy->mapSelectionToSource(sel); + } else { + log::error("FilterWidget::mapToSource() called, but proxy isn't set up"); + return sel; + } +} + +void FilterWidget::compile() +{ + Compiled compiled; + + if (s_options.useRegex) { + QRegularExpression::PatternOptions flags = + QRegularExpression::DotMatchesEverythingOption; + + if (!s_options.regexCaseSensitive) { + flags |= QRegularExpression::CaseInsensitiveOption; + } + + if (s_options.regexExtended) { + flags |= QRegularExpression::ExtendedPatternSyntaxOption; + } + + compiled.push_back({QRegularExpression(m_text, flags)}); + } else { + const QStringList ORList = [&] { + QString filterCopy = QString(m_text); + filterCopy.replace("||", ";").replace("OR", ";").replace("|", ";"); + return filterCopy.split(";", Qt::SkipEmptyParts); + }(); + + // split in ORSegments that internally use AND logic + for (const auto& ORSegment : ORList) { + const auto keywords = ORSegment.split(" ", Qt::SkipEmptyParts); + QList regexes; + + for (const auto& keyword : keywords) { + const QString escaped = QRegularExpression::escape(keyword); + + const auto flags = QRegularExpression::CaseInsensitiveOption | + QRegularExpression::DotMatchesEverythingOption; + + regexes.push_back(QRegularExpression(escaped, flags)); + } + + compiled.push_back(regexes); + } + } + + bool valid = true; + + for (auto&& ANDKeywords : compiled) { + for (auto&& keyword : ANDKeywords) { + if (!keyword.isValid()) { + valid = false; + break; + } + } + + if (!valid) { + break; + } + } + + m_valid = valid; + setStyleProperty(m_edit, "valid-filter", m_valid); + + if (m_valid) { + m_compiled = std::move(compiled); + } +} + +bool FilterWidget::matches(predFun pred) const +{ + if (m_compiled.isEmpty() || !pred) { + return true; + } + + for (auto& ANDKeywords : m_compiled) { + bool segmentGood = true; + + // check each word in the segment for match, each word needs to be matched + // but it doesn't matter where. + for (auto& currentKeyword : ANDKeywords) { + if (!pred(currentKeyword)) { + segmentGood = false; + } + } + + if (segmentGood) { + // the last AND loop didn't break so the ORSegments is true so mod + // matches filter + return true; + } + } + + return false; +} + +bool FilterWidget::matches(const QString& s) const +{ + return matches([&](const QRegularExpression& re) { + return re.match(s).hasMatch(); + }); +} + +void FilterWidget::hookEdit() +{ + m_eventFilter = new EventFilter(m_edit, [&](auto* w, auto* e) { + if (e->type() == QEvent::Resize) { + onResized(); + } else if (e->type() == QEvent::ContextMenu) { + onContextMenu(w, static_cast(e)); + return true; + } + + return false; + }); + + m_edit->installEventFilter(m_eventFilter); + QObject::connect(m_edit, &QLineEdit::textChanged, [&] { + onTextChanged(); + }); +} + +void FilterWidget::unhookEdit() +{ + if (m_clear) { + delete m_clear; + m_clear = nullptr; + } + + if (m_edit) { + m_edit->removeEventFilter(m_eventFilter); + } +} + +void FilterWidget::hookList() +{ + m_proxy = new FilterWidgetProxyModel(*this); + m_proxy->setSourceModel(m_list->model()); + m_list->setModel(m_proxy); + + setShortcuts(); +} + +void FilterWidget::unhookList() +{ + if (m_proxy && m_list) { + auto* model = m_proxy->sourceModel(); + m_proxy->setSourceModel(nullptr); + delete m_proxy; + + m_list->setModel(model); + } + + for (auto* s : m_shortcuts) { + delete s; + } + + m_shortcuts.clear(); +} + +// walks up the parents of `w`, returns true if one of them is a QDialog +// +bool topLevelIsDialog(QWidget* w) +{ + if (!w) { + return false; + } + + auto* p = w->parentWidget(); + while (p) { + if (dynamic_cast(p)) { + return true; + } + + p = p->parentWidget(); + } + + return false; +} + +void FilterWidget::setShortcuts() +{ + auto activate = [this] { + onFind(); + }; + + auto reset = [this] { + onReset(); + }; + + auto hookActivate = [&](auto* w) { + auto* s = new QShortcut(QKeySequence::Find, w); + s->setAutoRepeat(false); + s->setContext(Qt::WidgetWithChildrenShortcut); + QObject::connect(s, &QShortcut::activated, activate); + m_shortcuts.push_back(s); + }; + + auto hookReset = [&](auto* w) { + auto* s = new QShortcut(QKeySequence(Qt::Key_Escape), w); + s->setAutoRepeat(false); + s->setContext(Qt::WidgetWithChildrenShortcut); + QObject::connect(s, &QShortcut::activated, reset); + m_shortcuts.push_back(s); + }; + + // don't hook the escape key for reset when the filter is in a dialog, the + // standard behaviour is to close the dialog + const bool inDialog = + topLevelIsDialog(m_list ? static_cast(m_list) : m_edit); + + if (m_list) { + hookActivate(m_list); + + if (!inDialog) { + hookReset(m_list); + } + } + + if (m_edit) { + hookActivate(m_edit); + + if (!inDialog) { + hookReset(m_edit); + } + } +} + +void FilterWidget::onFind() +{ + if (m_edit) { + m_edit->setFocus(); + m_edit->selectAll(); + } +} + +void FilterWidget::onReset() +{ + clear(); + + if (m_list) { + m_list->setFocus(); + } +} + +void FilterWidget::createClear() +{ + m_clear = new QToolButton(m_edit); + + QPixmap pixmap(":/MO/gui/edit_clear"); + m_clear->setIcon(QIcon(pixmap)); + m_clear->setIconSize(pixmap.size()); + m_clear->setCursor(Qt::ArrowCursor); + m_clear->setStyleSheet("QToolButton { border: none; padding: 0px; }"); + m_clear->hide(); + + QObject::connect(m_clear, &QToolButton::clicked, [&] { + clear(); + }); + + repositionClearButton(); +} + +void FilterWidget::set() +{ + const QString old = m_text; + + QString currentText; + if (m_edit != nullptr) { + currentText = m_edit->text(); + } else { + currentText = m_text; + } + + emit aboutToChange(old, currentText); + + m_text = currentText; + compile(); + + if (m_proxy) { + m_proxy->refreshFilter(); + } + + if (m_list) { + if (m_filteredBorder) { + setStyleProperty(m_list, "filtered", !m_text.isEmpty()); + } + + scrollToSelection(); + } + + emit changed(old, currentText); +} + +void FilterWidget::update() +{ + if (!m_text.isEmpty()) { + set(); + } +} + +void FilterWidget::onTextChanged() +{ + m_clear->setVisible(!m_edit->text().isEmpty()); + + const auto text = m_edit->text(); + if (text == m_text) { + return; + } + + if (m_useDelay) { + m_timer->start(500); + } else { + set(); + } +} + +void FilterWidget::onResized() +{ + repositionClearButton(); +} + +void FilterWidget::onContextMenu(QObject*, QContextMenuEvent* e) +{ + std::unique_ptr m(m_edit->createStandardContextMenu()); + m->setParent(m_edit); + + auto* title = new QAction(tr("Filter options"), m_edit); + title->setEnabled(false); + + auto f = title->font(); + f.setBold(true); + title->setFont(f); + + auto* regex = new QAction(tr("Use regular expressions"), m_edit); + regex->setStatusTip(tr("Use regular expressions in filters")); + regex->setCheckable(true); + regex->setChecked(s_options.useRegex); + + connect(regex, &QAction::triggered, [&] { + s_options.useRegex = regex->isChecked(); + update(); + }); + + auto* cs = new QAction(tr("Case sensitive"), m_edit); + //: leave "(/i)" verbatim + cs->setStatusTip(tr("Make regular expressions case sensitive (/i)")); + cs->setCheckable(true); + cs->setChecked(s_options.regexCaseSensitive); + cs->setEnabled(s_options.useRegex); + + connect(cs, &QAction::triggered, [&] { + s_options.regexCaseSensitive = cs->isChecked(); + update(); + }); + + auto* x = new QAction(tr("Extended"), m_edit); + //: leave "(/x)" verbatim + x->setStatusTip(tr("Ignores unescaped whitespace in regular expressions (/x)")); + x->setCheckable(true); + x->setChecked(s_options.regexExtended); + x->setEnabled(s_options.useRegex); + + connect(x, &QAction::triggered, [&] { + s_options.regexExtended = x->isChecked(); + update(); + }); + + auto* sts = new QAction(tr("Keep selection in view"), m_edit); + sts->setStatusTip(tr("Scroll to keep the current selection in view after filtering")); + sts->setCheckable(true); + sts->setChecked(s_options.scrollToSelection); + + connect(sts, &QAction::triggered, [&] { + s_options.scrollToSelection = sts->isChecked(); + update(); + }); + + m->insertSeparator(m->actions().first()); + m->insertAction(m->actions().first(), x); + m->insertAction(m->actions().first(), cs); + m->insertAction(m->actions().first(), regex); + m->insertAction(m->actions().first(), sts); + m->insertAction(m->actions().first(), title); + + m->exec(e->globalPos()); +} + +void FilterWidget::repositionClearButton() +{ + if (!m_clear) { + return; + } + + const QSize sz = m_clear->sizeHint(); + const int frame = m_edit->style()->pixelMetric(QStyle::PM_DefaultFrameWidth); + const auto r = m_edit->rect(); + + const auto x = r.right() - frame - sz.width(); + const auto y = (r.bottom() + 1 - sz.height()) / 2; + + m_clear->move(x, y); +} + +} // namespace MOBase diff --git a/libs/uibase/src/finddialog.cpp b/libs/uibase/src/finddialog.cpp new file mode 100644 index 0000000..9284014 --- /dev/null +++ b/libs/uibase/src/finddialog.cpp @@ -0,0 +1,51 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 +#include "ui_finddialog.h" + +namespace MOBase +{ + +FindDialog::FindDialog(QWidget* parent) : QDialog(parent), ui(new Ui::FindDialog) +{ + ui->setupUi(this); +} + +FindDialog::~FindDialog() +{ + delete ui; +} + +void FindDialog::on_nextBtn_clicked() +{ + emit findNext(); +} + +void FindDialog::on_patternEdit_textChanged(const QString& pattern) +{ + emit patternChanged(pattern); +} + +void FindDialog::on_closeBtn_clicked() +{ + this->close(); +} +} // namespace MOBase diff --git a/libs/uibase/src/finddialog.ui b/libs/uibase/src/finddialog.ui new file mode 100644 index 0000000..b31145d --- /dev/null +++ b/libs/uibase/src/finddialog.ui @@ -0,0 +1,76 @@ + + + FindDialog + + + + 0 + 0 + 400 + 72 + + + + Find + + + + + + + + + + Find what: + + + + + + + Search term + + + Search term + + + + + + + + + + + + + Find next occurence from current file position. + + + Find next occurence from current file position. + + + &Find Next + + + + + + + Close + + + Close + + + Close + + + + + + + + + + diff --git a/libs/uibase/src/guessedvalue.cpp b/libs/uibase/src/guessedvalue.cpp new file mode 100644 index 0000000..c8393af --- /dev/null +++ b/libs/uibase/src/guessedvalue.cpp @@ -0,0 +1 @@ +#include diff --git a/libs/uibase/src/ifiletree.cpp b/libs/uibase/src/ifiletree.cpp new file mode 100644 index 0000000..b582d18 --- /dev/null +++ b/libs/uibase/src/ifiletree.cpp @@ -0,0 +1,1029 @@ +#include + +#include +#include +#include +#include + +#include + +// FileTreeEntry: +namespace MOBase +{ +FileTreeEntry::FileTreeEntry(std::shared_ptr parent, QString name) + : m_Parent(parent), m_Name(name) +{} + +QString FileTreeEntry::suffix() const +{ + const qsizetype idx = m_Name.lastIndexOf("."); + return (isDir() || idx == -1) ? "" : m_Name.mid(idx + 1); +} + +bool FileTreeEntry::hasSuffix(QString suffix) const +{ + return this->suffix().compare(suffix, FileNameComparator::CaseSensitivity) == 0; +} + +bool FileTreeEntry::hasSuffix(QStringList suffixes) const +{ + return suffixes.contains(suffix(), FileNameComparator::CaseSensitivity); +} + +QString FileTreeEntry::pathFrom(std::shared_ptr tree, + QString sep) const +{ + + // We will construct the path from right to left: + QString path = name(); + + auto p = parent(); + while (p != nullptr && p != tree) { + // We need to check the parent, otherwize we are going to prepend the name + // and a / for the base, which we do not want. + if (p->parent() != nullptr) { + path = p->name() + sep + path; + } + p = p->parent(); + } + + return p == tree ? path : QString(); +} + +bool FileTreeEntry::detach() +{ + auto p = parent(); + if (p == nullptr) { + return false; + } + return p->erase(shared_from_this()) != p->end(); +} + +bool FileTreeEntry::moveTo(std::shared_ptr tree) +{ + return tree->insert(shared_from_this()) != tree->end(); +} + +std::shared_ptr FileTreeEntry::clone() const +{ + return createFileEntry(nullptr, name()); +} + +std::shared_ptr +FileTreeEntry::createFileEntry(std::shared_ptr parent, QString name) +{ + return std::shared_ptr(new FileTreeEntry(parent, name)); +} +} // namespace MOBase + +// IFileTree: +namespace MOBase +{ + +/** + * Comparator for file entries. + */ +struct FileEntryComparator +{ + bool operator()(std::shared_ptr const& a, + std::shared_ptr const& b) const + { + if (a->isDir() && !b->isDir()) { + return true; + } else if (!a->isDir() && b->isDir()) { + return false; + } else { + return FileNameComparator::compare(a->name(), b->name()) < 0; + } + } +}; + +/** + * @brief Comparator that can be used to find entry matching the given name and + * file type. + */ +struct MatchEntryComparator +{ + + MatchEntryComparator(QString const& name, FileTreeEntry::FileTypes matchTypes) + : m_Name(name), m_MatchTypes(matchTypes) + {} + + bool operator()(const std::shared_ptr& fileEntry) const + { + return m_MatchTypes.testFlag(fileEntry->fileType()) && + fileEntry->compare(m_Name) == 0; + } + + bool operator()(const std::shared_ptr& fileEntry) const + { + return m_MatchTypes.testFlag(fileEntry->fileType()) && + fileEntry->compare(m_Name) == 0; + } + +private: + QString const& m_Name; + FileTreeEntry::FileTypes m_MatchTypes; +}; + +/** + * + */ +bool IFileTree::exists(QString path, FileTypes type) const +{ + return find(path, type) != nullptr; +} + +/** + * + */ +std::shared_ptr IFileTree::find(QString path, FileTypes type) +{ + return fetchEntry(splitPath(path), type); +} +std::shared_ptr IFileTree::find(QString path, FileTypes type) const +{ + return fetchEntry(splitPath(path), type); +} + +/** + * + */ +void IFileTree::walk( + std::function)> + callback, + QString sep) const +{ + + std::stack>> stack; + + // We start by pushing all the entries in this tree, this avoid having to do extra + // check later for avoid leading separator: + for (auto rit = rbegin(); rit != rend(); ++rit) { + stack.push({"", *rit}); + } + + while (!stack.empty()) { + auto [path, entry] = stack.top(); + stack.pop(); + + auto res = callback(path, entry); + if (res == WalkReturn::STOP) { + break; + } + if (entry->isDir() && res != WalkReturn::SKIP) { + auto tree = entry->astree(); + for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) { + stack.push({path + tree->name() + sep, *rit}); + } + } + } +} + +/** + * + */ +std::shared_ptr IFileTree::addFile(QString path, bool replaceIfExists) +{ + QStringList parts = splitPath(path); + + // Check if the file already exists: + auto existingEntry = fetchEntry(parts, IFileTree::FILE_OR_DIRECTORY); + if (!replaceIfExists && existingEntry != nullptr) { + return nullptr; + } + + // Find or create the tree: + IFileTree* tree; + + if (parts.size() > 1) { + + // Create the tree: + std::shared_ptr treeEntry = + createTree(parts.begin(), parts.end() - 1); + + // Early fail if the tree was not created: + if (treeEntry == nullptr) { + return nullptr; + } + + tree = treeEntry->astree().get(); + } else { + tree = this; + } + + std::shared_ptr entry = + tree->makeFile(tree->astree(), parts[parts.size() - 1]); + + // If makeFile returns a null pointer, it means we cannot create file: + if (entry == nullptr) { + return nullptr; + } + + // Remove the existing files if there was one: + if (existingEntry) { + existingEntry->detach(); + } + + // Insert in the tree: + tree->entries().insert( + std::upper_bound(tree->begin(), tree->end(), entry, FileEntryComparator{}), + entry); + + return entry; +} + +/** + * + */ +std::shared_ptr IFileTree::addDirectory(QString path) +{ + QStringList parts = splitPath(path); + return createTree(parts.begin(), parts.end()); +} + +/** + * + */ +IFileTree::iterator IFileTree::insert(std::shared_ptr entry, + InsertPolicy insertPolicy) +{ + + // Check that this is not the current tree or a parent tree: + if (entry->isDir()) { + std::shared_ptr tmp = astree(); + while (tmp != nullptr) { + if (tmp == entry->astree()) { + return end(); + } + tmp = tmp->parent(); + } + } + + // Check if there exists an entry with the same name: + auto existingIt = std::find_if( + begin(), end(), MatchEntryComparator{entry->name(), FILE_OR_DIRECTORY}); + + // Already in the tree? + if (existingIt != end() && *existingIt == entry) { + return existingIt; + } + + auto insertionIt = end(); + + if (existingIt != end()) { + if (insertPolicy == InsertPolicy::FAIL_IF_EXISTS) { + return end(); + } + + // We replace if the policy is REPLACE or if the new and old entry are + // both files: + if (insertPolicy == InsertPolicy::REPLACE || + ((*existingIt)->isFile() && entry->isFile())) { + if (beforeReplace(this, existingIt->get(), entry.get())) { + // Detach the old entry from its parent (not using .detach() + // to remove the entry since we are replacing it): + (*existingIt)->m_Parent.reset(); + entries().erase(existingIt); + + // insert at the right place + insertionIt = entries().insert( + std::lower_bound(begin(), end(), entry, FileEntryComparator{}), entry); + } else { + return end(); + } + } else if ((*existingIt)->isFile() || entry->isFile()) { + // If we arrive here and one of the entry is a file, we fail: + return end(); + } else { + // If we end up here, we know that the policy is MERGE and that both + // are directory that can be merged: + mergeTree((*existingIt)->astree(), entry->astree(), nullptr); + insertionIt = existingIt; + } + } else if (beforeInsert(this, entry.get())) { + insertionIt = entries().insert( + std::lower_bound(begin(), end(), entry, FileEntryComparator{}), entry); + } + + // Remove the tree from its parent (parent() can be null if we are inserting + // a new tree): + if (entry->parent() != nullptr) { + entry->parent()->erase(entry); + } + + // If the entry was actually inserted, we update its parent: + if (*insertionIt == entry) { + entry->m_Parent = astree(); + } + // Otherwize, we reset it (if this was a merge operation): + else { + entry->m_Parent.reset(); + } + + return insertionIt; +} + +/** + * + */ +bool IFileTree::move(std::shared_ptr entry, QString path, + InsertPolicy insertPolicy) +{ + + // Check that this is not a parent tree: + if (entry->isDir()) { + std::shared_ptr tmp = parent(); + while (tmp != nullptr) { + if (tmp == entry->astree()) { + return false; + } + tmp = tmp->parent(); + } + } + + // Insert in folder or replace: + const bool insertFolder = path.isEmpty() || path.endsWith("/") || path.endsWith("\\"); + + // Retrieve the path: + QStringList parts = splitPath(path); + + // Backup the entry name (in case the insertion fails), and update the + // name: + QString entryName = entry->m_Name; + if (!insertFolder) { + entry->m_Name = parts.takeLast(); + } + + // Find or create the tree: + IFileTree* tree; + + if (!parts.isEmpty()) { + + // Create the tree: + std::shared_ptr treeEntry = createTree(parts.begin(), parts.end()); + + // Early fail if the tree was not created: + if (treeEntry == nullptr) { + return false; + } + + tree = treeEntry->astree().get(); + } else { + tree = this; + } + + // We try to insert, and if it fails we need to reset the name: + auto it = tree->insert(entry, insertPolicy); + if (it == tree->end()) { + entry->m_Name = entryName; + return false; + } + + return true; +} + +/** + * + */ +std::shared_ptr +IFileTree::copy(std::shared_ptr entry, QString path, + InsertPolicy insertPolicy) +{ + // Note: If a conflict exists, the tree is cloned before checking the conflict, so + // this is not the most efficient way but copying tree should be pretty rare (and + // probably avoided anyway), and this allow us to use `move()` to do all the complex + // operations. + auto clone = entry->clone(); + if (move(clone, path, insertPolicy)) { + return clone; + } + return nullptr; +} + +/** + * + */ +std::size_t IFileTree::merge(std::shared_ptr source, + OverwritesType* overwrites) +{ + + // Check that this is not a parent tree: + std::shared_ptr tmp = astree(); + while (tmp != nullptr) { + if (tmp == source) { + return MERGE_FAILED; + } + tmp = tmp->parent(); + } + + return mergeTree(astree(), source, overwrites); +} + +/** + * + */ +IFileTree::iterator IFileTree::erase(std::shared_ptr entry) +{ + + if (!beforeRemove(this, entry.get())) { + return end(); + } + + auto it = std::find(begin(), end(), entry); + if (it == end()) { + return it; + } + entry->m_Parent.reset(); + return entries().erase(it); + ; +} + +/** + * + */ +std::pair> +IFileTree::erase(QString name) +{ + auto it = std::find_if(begin(), end(), [&name](const auto& entry) { + return entry->compare(name) == 0; + }); + + if (it == end()) { + return {it, nullptr}; + } + + if (!beforeRemove(this, it->get())) { + return {end(), nullptr}; + } + + // Save the entry to return it: + auto entry = *it; + entry->m_Parent.reset(); + + return {entries().erase(it), entry}; +} + +/** + * + */ +bool IFileTree::clear() +{ + // Need to find the iterator up to which we should erase: + auto& entries_ = entries(); + auto it = entries_.begin(); + for (; it != entries_.end() && beforeRemove(this, it->get()); ++it) { + // Detach (but not remove from the vector): + (*it)->m_Parent.reset(); + } + entries_.erase(entries_.begin(), it); + return empty(); +} + +/** + * + */ +std::size_t IFileTree::removeAll(QStringList names) +{ + return removeIf([&names](auto& entry) { + return names.contains(entry->name(), Qt::CaseInsensitive); + }); +} + +/** + * + */ +std::size_t IFileTree::removeIf( + std::function const&)> predicate) +{ + std::size_t osize = size(); + auto& en = entries(); + // Cannot use begin() and end() directly because those are immutable iterators: + en.erase(std::remove_if(en.begin(), en.end(), + [this, &predicate](auto& entry) { + return beforeRemove(this, entry.get()) && predicate(entry); + }), + en.end()); + return osize - size(); +} + +/** + * + */ +QStringList IFileTree::splitPath(QString path) +{ + // Using raw \\ instead of QDir::separator() since we are replacing by / + // anyway, and this avoid pulling an extra header (like QDir) only + // for the separator. + return path.replace("\\", "/").split("/", Qt::SkipEmptyParts); +} + +/** + * + */ +bool IFileTree::beforeReplace(IFileTree const*, FileTreeEntry const*, + FileTreeEntry const*) +{ + return true; +} + +/** + * + */ +bool IFileTree::beforeInsert(IFileTree const*, FileTreeEntry const*) +{ + return true; +} + +/** + * + */ +bool IFileTree::beforeRemove(IFileTree const*, FileTreeEntry const*) +{ + return true; +} + +/** + * + */ +std::size_t IFileTree::mergeTree(std::shared_ptr destination, + std::shared_ptr source, + OverwritesType* overwrites) +{ + + const auto comp = FileEntryComparator{}; + + // Number of overwritten entries: + std::size_t noverwrites = 0; + + // Note: Using the iterators from the vector directly since the other ones + // cannot be assigned to. + auto &dstEntries = destination->entries(), &srcEntries = source->entries(); + + // Note: Since entries are not sorted by name but by type (file/directory) + // then name, there is no fast way to check for conflict. + for (auto& srcEntry : srcEntries) { + + // Try to find an exact match (name and type) - This iterator also + // serve to know where the entry should be inserted: + auto dstIt = std::lower_bound(dstEntries.begin(), dstEntries.end(), srcEntry, comp); + + // Exact match found: + if (dstIt != dstEntries.end() && (*dstIt)->compare(srcEntry->name()) == 0 && + (*dstIt)->isFile() == srcEntry->isFile()) { + + // Both directory, we merge: + if ((*dstIt)->isDir() && srcEntry->isDir()) { + noverwrites += mergeTree((*dstIt)->astree(), srcEntry->astree(), overwrites); + + // Detach the entry: + srcEntry->m_Parent.reset(); + } + // Otherwize, check if the source can replace the destination: + else if (beforeReplace(destination.get(), dstIt->get(), srcEntry.get())) { + // Remove the parent: + auto dstEntry = *dstIt; + dstEntry->m_Parent.reset(); + + // Update overwrites information: + noverwrites++; + if (overwrites != nullptr) { + overwrites->insert({dstEntry, srcEntry}); + } + + // Replace the destination: + *dstIt = srcEntry; + srcEntry->m_Parent = destination; + } + // If not, fails: + else { + return MERGE_FAILED; + } + } else { + // If we did not find a match, the only way to check is to look + // through the vector: + auto conflictIt = std::find_if(dstEntries.begin(), dstEntries.end(), + [name = srcEntry->name()](auto const& dstEntry) { + return dstEntry->compare(name) == 0; + }); + + // Conflict (note that here both entries are of different types, so no need to + // check if we replace or merge): + int deleteIndex = -1; + if (conflictIt != dstEntries.end()) { + + // We check if we can replace the entry: + if (!beforeReplace(destination.get(), conflictIt->get(), srcEntry.get())) { + return MERGE_FAILED; + } + + // We need to store the index because the insert() will mess up the + // iterators: + deleteIndex = static_cast(conflictIt - std::begin(dstEntries)); + if (dstIt < conflictIt) { + deleteIndex += 1; + } + + // Detach the conflicting entry (we erase it later, after the insertion): + (*conflictIt)->m_Parent.reset(); + + // Update overwrites information: + noverwrites++; + if (overwrites != nullptr) { + overwrites->insert({*conflictIt, srcEntry}); + } + } + // No conflict, we still have to check if we can insert: + else if (!beforeInsert(destination.get(), srcEntry.get())) { + return MERGE_FAILED; + } + + // Insert the entry using the previous iterator: + dstEntries.insert(dstIt, srcEntry); + + // We delete here: + if (deleteIndex != -1) { + dstEntries.erase(dstEntries.begin() + deleteIndex); + } + + // Update the parent: + srcEntry->m_Parent = destination; + } + } + + // Clear the sources: + srcEntries.clear(); + + return noverwrites; +} + +/** + * + */ +std::shared_ptr IFileTree::createOrphanTree(QString name) const +{ + auto directory = makeDirectory(nullptr, name); + if (directory != nullptr) { + directory->m_Populated = true; + } + return directory; +} + +/** + * + */ +std::shared_ptr IFileTree::fetchEntry(QStringList path, + FileTypes matchTypes) +{ + return std::const_pointer_cast( + const_cast(this)->fetchEntry(path, matchTypes)); +} +std::shared_ptr IFileTree::fetchEntry(QStringList const& path, + FileTypes matchTypes) const +{ + // Check to ensure that the path contains at least one element: + if (path.isEmpty()) { + return nullptr; + } + + // Early check: + if (path[path.size() - 1].startsWith("*")) { + return nullptr; + } + + const IFileTree* tree = this; + auto it = std::begin(path); + for (; tree != nullptr && it != std::end(path) - 1; ++it) { + // Special cases: + if (*it == ".") { + continue; + } else if (*it == "..") { + tree = tree->parent().get(); + } else { + // Find the entry at the current level: + auto entryIt = std::find_if(tree->begin(), tree->end(), + MatchEntryComparator{*it, IFileTree::DIRECTORY}); + + // Early exists if the entry does not exist or is not a directory: + if (entryIt == tree->end()) { + tree = nullptr; + } else { + tree = (*entryIt)->astree().get(); + } + } + } + + if (tree == nullptr) { + return nullptr; + } + + // We have the final tree: + auto entryIt = + std::find_if(tree->begin(), tree->end(), MatchEntryComparator{*it, matchTypes}); + auto bIt = tree->end(); + return entryIt == bIt ? nullptr : *entryIt; +} + +/** + * @brief Create a new file under this tree. + * + * @param parent The current tree, without const-qualification. + * @param name Name of the file. + * @param time Modification time of the file. + * + * @return the created file. + */ +std::shared_ptr +IFileTree::makeFile(std::shared_ptr parent, QString name) const +{ + return createFileEntry(parent, name); +} + +/** + * + */ +IFileTree::IFileTree() {} + +/** + * + */ +std::shared_ptr IFileTree::clone() const +{ + std::shared_ptr tree = doClone(); + + // Don't copy not populated tree, it is not useful: + if (m_Populated) { + tree->m_Populated = true; + auto& tentries = tree->m_Entries; + for (auto e : entries()) { + auto ce = e->clone(); + ce->m_Parent = tree; + tentries.push_back(ce); + } + } + + return tree; +} + +/** + * + */ +std::shared_ptr IFileTree::createTree(QStringList::const_iterator begin, + QStringList::const_iterator end) +{ + // The current tree and entry: + std::shared_ptr tree = astree(); + for (auto it = begin; tree != nullptr && it != end; ++it) { + // Special cases: + if (*it == ".") { + continue; + } else if (*it == "..") { + // parent() returns nullptr if it does not exist, so no + // check required: + tree = parent(); + } else { + + // Check if the entry exists (looking for both files and directories + // because we don't want to override a file): + auto entryIt = + std::find_if(tree->begin(), tree->end(), + MatchEntryComparator{*it, IFileTree::FILE_OR_DIRECTORY}); + + // Create if it does not: + if (entryIt == tree->end()) { + auto newTree = tree->makeDirectory(tree, *it); + + // If makeDirectory returns a null pointer, it means we cannot create tree. + if (newTree == nullptr) { + tree = nullptr; + break; + } + + // The tree is empty so already populated: + newTree->m_Populated = true; + + tree->entries().insert(std::upper_bound(tree->begin(), tree->end(), newTree, + FileEntryComparator{}), + newTree); + tree = newTree; + } else if ((*entryIt)->isDir()) { + tree = (*entryIt)->astree(); + } else { // Cannot go further: + tree = nullptr; + } + } + } + + return tree; +} + +/** + * @brief Retrieve the vector of entries after populating it if required. + * + * @return the vector of entries. + */ +std::vector>& IFileTree::entries() +{ + std::call_once(m_OnceFlag, [this]() { + populate(); + }); + return m_Entries; +} +const std::vector>& IFileTree::entries() const +{ + std::call_once(m_OnceFlag, [this]() { + populate(); + }); + return m_Entries; +} + +/** + * @brief Populate the internal vectors and update the flag. + */ +void IFileTree::populate() const +{ + // Need to check m_Populated again here since the tree can be populated without + // a call to entries() (e.g., on copy/orphanTree): + if (!m_Populated) { + if (!doPopulate(astree(), m_Entries)) { + std::sort(std::begin(m_Entries), std::end(m_Entries), FileEntryComparator{}); + } + m_Populated = true; + } +} + +// walk and glob with generator + +std::generator> +walk(std::shared_ptr fileTree) +{ + std::stack> stack; + + // we start by pushing all the entries in this tree, this avoid having to do extra + // check later to avoid leading separator + for (auto rit = fileTree->rbegin(); rit != fileTree->rend(); ++rit) { + stack.push(*rit); + } + + while (!stack.empty()) { + auto entry = stack.top(); + stack.pop(); + + co_yield entry; + + if (entry->isDir()) { + auto tree = entry->astree(); + for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) { + stack.push(*rit); + } + } + } +} + +namespace +{ + using glob_stack_t = std::stack< + std::pair, std::span>>; + + // check the given entry against the list of patterns and add new (entry, patterns) to + // the given stack + // + std::generator> + ifiletree_glob_impl_step(glob_stack_t& glob_stack, + std::shared_ptr entry, + std::span const& patterns) + { + // no more patterns, nothing to do + if (patterns.size() == 0) { + co_return; + } + + // special handling if the pattern is empty (correspond to a '**' glob) + if (patterns[0].pattern() == "") { + + // if there are more patterns after '**', we need to check the entry again, e.g., + // if the entry name is 'x' and the pattern is '**/x' to match it + if (patterns.size() != 1) { + glob_stack.emplace(entry, patterns.subspan(1)); + } + + // if the entry is a file, there is nothing to do with '**' + if (entry->isFile()) { + co_return; + } + + // if this is the end of the patterns list, we need to yield the current entry + // since it is a directory + if (patterns.size() == 1) { + co_yield entry; + } + + // recurse over childs, but for directories, we need to keep the leading '**' in + // the list of patterns since '**' can match multiple level of directories + auto tree = entry->astree(); + for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) { + glob_stack.emplace(*rit, (*rit)->isDir() ? patterns : patterns.subspan(1)); + } + } + // otherwise (if the first patterns is not '**'), we simply check if we have a match + else if (patterns[0].match(entry->name()).hasMatch()) { + // this was the last pattern and we have a match, so we yield the current entry, + // not that this will yield intermediate matching directory, but this is expected + if (patterns.size() == 1) { + co_yield entry; + } + + // if the entry is not a directory, we have nothing more to do + if (entry->isFile()) { + co_return; + } + + // if all that remain after this pattern is a '**', we need to yield the current + // entry since '**' can also match an empty succession of directories, e.g. 'a/b' + // is matched by 'a/b/**' + if (patterns.size() == 2 && patterns[1].pattern() == "") { + co_yield entry; + } + + // we then need to recurse over + auto tree = entry->astree(); + for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) { + glob_stack.emplace(*rit, patterns.subspan(1)); + } + } + } + + // main function for IFileTree::glob - this function simply manages the stack of of + // entry / patterns to handle, and then falls back to ifiletree_glob_impl_step + // + std::generator> + ifiletree_glob_impl(std::shared_ptr entry, + std::span const& patterns) + { + glob_stack_t stack; + stack.emplace(entry, patterns); + + while (!stack.empty()) { + const auto [childEntry, childPatterns] = stack.top(); + stack.pop(); + co_yield std::ranges::elements_of( + ifiletree_glob_impl_step(stack, childEntry, childPatterns)); + } + } + +} // namespace + +/** + * + */ +std::generator> +glob(std::shared_ptr fileTree, QString pattern, + GlobPatternType patternType) +{ + // replace \\ by / to simply handling + pattern = pattern.trimmed().replace("\\", "/"); + + // reduce successions of **/** to **, this makes it easier to handle it in the + // actual implementation + pattern = pattern.replace(QRegularExpression("(\\*\\*/)*\\*\\*"), "**"); + + // we are going to match directly starting from the child of the tree, so we need + // to handle the root here + // + // note: this is the only pattern that can match the tree itself + if (pattern == "**") { + co_yield fileTree; + } + + // split pattern into blocks, we keep + const auto regexOptions = FileNameComparator::CaseSensitivity == Qt::CaseInsensitive + ? QRegularExpression::CaseInsensitiveOption + : QRegularExpression::NoPatternOption; + std::vector patterns; + for (const auto& part : pattern.split("/")) { + if (part == "**") { + // for '**' pattern, push an empty regex + patterns.emplace_back(); + } else { + const auto regexpPattern = + patternType == GlobPatternType::GLOB + ? QRegularExpression::wildcardToRegularExpression( + part, QRegularExpression::NonPathWildcardConversion) + : part; + const QRegularExpression regex(regexpPattern, regexOptions); + + if (!regex.isValid()) { + throw InvalidGlobPatternException(regex.errorString()); + } + + patterns.push_back(regex); + } + } + + // fall back to the main glob function for the child of this tree + for (const auto& child : *fileTree) { + co_yield std::ranges::elements_of(ifiletree_glob_impl(child, patterns)); + } +} + +} // namespace MOBase diff --git a/libs/uibase/src/imodrepositorybridge.cpp b/libs/uibase/src/imodrepositorybridge.cpp new file mode 100644 index 0000000..9b610b0 --- /dev/null +++ b/libs/uibase/src/imodrepositorybridge.cpp @@ -0,0 +1 @@ +#include diff --git a/libs/uibase/src/imoinfo.cpp b/libs/uibase/src/imoinfo.cpp new file mode 100644 index 0000000..faa2db9 --- /dev/null +++ b/libs/uibase/src/imoinfo.cpp @@ -0,0 +1,23 @@ +#include + +namespace MOBase +{ + +static QString g_pluginDataPath; + +QString IOrganizer::getPluginDataPath() +{ + return g_pluginDataPath; +} + +} // namespace MOBase + +namespace MOBase::details +{ + +void setPluginDataPath(const QString& s) +{ + g_pluginDataPath = s; +} + +} // namespace MOBase::details diff --git a/libs/uibase/src/json.cpp b/libs/uibase/src/json.cpp new file mode 100644 index 0000000..49d5fb3 --- /dev/null +++ b/libs/uibase/src/json.cpp @@ -0,0 +1,557 @@ +/** + * QtJson - A simple class for parsing JSON data into a QVariant hierarchies and + * vice-versa. Copyright (C) 2011 Eeli Reilin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file json.cpp + */ + +#include + +namespace QtJson +{ +static QString sanitizeString(QString str); +static QByteArray join(const QList& list, const QByteArray& sep); +static QVariant parseValue(const QString& json, int& index, bool& success); +static QVariant parseObject(const QString& json, int& index, bool& success); +static QVariant parseArray(const QString& json, int& index, bool& success); +static QVariant parseString(const QString& json, int& index, bool& success); +static QVariant parseNumber(const QString& json, int& index); +static int lastIndexOfNumber(const QString& json, int index); +static void eatWhitespace(const QString& json, int& index); +static int lookAhead(const QString& json, int index); +static int nextToken(const QString& json, int& index); + +template +QByteArray serializeMap(const T& map, bool& success) +{ + QByteArray str = "{ "; + QList pairs; + for (typename T::const_iterator it = map.begin(), itend = map.end(); it != itend; + ++it) { + QByteArray serializedValue = serialize(it.value()); + if (serializedValue.isNull()) { + success = false; + break; + } + pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue; + } + + str += join(pairs, ", "); + str += " }"; + return str; +} + +/** + * parse + */ +QVariant parse(const QString& json) +{ + bool success = true; + return parse(json, success); +} + +/** + * parse + */ +QVariant parse(const QString& json, bool& success) +{ + success = true; + + // Return an empty QVariant if the JSON data is either null or empty + if (!json.isNull() || !json.isEmpty()) { + QString data = json; + // We'll start from index 0 + int index = 0; + + // Parse the first value + QVariant value = parseValue(data, index, success); + + // Return the parsed value + return value; + } else { + // Return the empty QVariant + return QVariant(); + } +} + +QByteArray serialize(const QVariant& data) +{ + bool success = true; + return serialize(data, success); +} + +QByteArray serialize(const QVariant& data, bool& success) +{ + QByteArray str; + success = true; + + if (!data.isValid()) { // invalid or null? + str = "null"; + } else if ((data.typeId() == QMetaType::Type::QVariantList) || + (data.typeId() == QMetaType::Type::QStringList)) { // variant is a list? + QList values; + const QVariantList list = data.toList(); + Q_FOREACH (const QVariant& v, list) { + QByteArray serializedValue = serialize(v); + if (serializedValue.isNull()) { + success = false; + break; + } + values << serializedValue; + } + + str = "[ " + join(values, ", ") + " ]"; + } else if (data.typeId() == QMetaType::Type::QVariantHash) { // variant is a hash? + str = serializeMap<>(data.toHash(), success); + } else if (data.typeId() == QMetaType::Type::QVariantMap) { // variant is a map? + str = serializeMap<>(data.toMap(), success); + } else if ((data.typeId() == QMetaType::Type::QString) || + (data.typeId() == + QMetaType::Type::QByteArray)) { // a string or a byte array? + str = sanitizeString(data.toString()).toUtf8(); + } else if (data.typeId() == QMetaType::Type::Double) { // double? + double value = data.toDouble(); + if ((value - value) == 0.0) { + str = QByteArray::number(value, 'g'); + if (!str.contains(".") && !str.contains("e")) { + str += ".0"; + } + } else { + success = false; + } + } else if (data.typeId() == QMetaType::Type::Bool) { // boolean value? + str = data.toBool() ? "true" : "false"; + } else if (data.typeId() == QMetaType::Type::ULongLong) { // large unsigned number? + str = QByteArray::number(data.value()); + } else if (data.canConvert()) { // any signed number? + str = QByteArray::number(data.value()); + } else if (data.canConvert()) { // TODO: this code is never executed + str = QString::number(data.value()).toUtf8(); + } else if (data.canConvert()) { // can value be converted to string? + // this will catch QDate, QDateTime, QUrl, ... + str = sanitizeString(data.toString()).toUtf8(); + } else { + success = false; + } + + if (success) { + return str; + } else { + return QByteArray(); + } +} + +QString serializeStr(const QVariant& data) +{ + return QString::fromUtf8(serialize(data)); +} + +QString serializeStr(const QVariant& data, bool& success) +{ + return QString::fromUtf8(serialize(data, success)); +} + +/** + * \enum JsonToken + */ +enum JsonToken +{ + JsonTokenNone = 0, + JsonTokenCurlyOpen = 1, + JsonTokenCurlyClose = 2, + JsonTokenSquaredOpen = 3, + JsonTokenSquaredClose = 4, + JsonTokenColon = 5, + JsonTokenComma = 6, + JsonTokenString = 7, + JsonTokenNumber = 8, + JsonTokenTrue = 9, + JsonTokenFalse = 10, + JsonTokenNull = 11 +}; + +static QString sanitizeString(QString str) +{ + str.replace(QLatin1String("\\"), QLatin1String("\\\\")); + str.replace(QLatin1String("\""), QLatin1String("\\\"")); + str.replace(QLatin1String("\b"), QLatin1String("\\b")); + str.replace(QLatin1String("\f"), QLatin1String("\\f")); + str.replace(QLatin1String("\n"), QLatin1String("\\n")); + str.replace(QLatin1String("\r"), QLatin1String("\\r")); + str.replace(QLatin1String("\t"), QLatin1String("\\t")); + return QString(QLatin1String("\"%1\"")).arg(str); +} + +static QByteArray join(const QList& list, const QByteArray& sep) +{ + QByteArray res; + Q_FOREACH (const QByteArray& i, list) { + if (!res.isEmpty()) { + res += sep; + } + res += i; + } + return res; +} + +/** + * parseValue + */ +static QVariant parseValue(const QString& json, int& index, bool& success) +{ + // Determine what kind of data we should parse by + // checking out the upcoming token + switch (lookAhead(json, index)) { + case JsonTokenString: + return parseString(json, index, success); + case JsonTokenNumber: + return parseNumber(json, index); + case JsonTokenCurlyOpen: + return parseObject(json, index, success); + case JsonTokenSquaredOpen: + return parseArray(json, index, success); + case JsonTokenTrue: + nextToken(json, index); + return QVariant(true); + case JsonTokenFalse: + nextToken(json, index); + return QVariant(false); + case JsonTokenNull: + nextToken(json, index); + return QVariant(); + case JsonTokenNone: + break; + } + + // If there were no tokens, flag the failure and return an empty QVariant + success = false; + return QVariant(); +} + +/** + * parseObject + */ +static QVariant parseObject(const QString& json, int& index, bool& success) +{ + QVariantMap map; + int token; + + // Get rid of the whitespace and increment index + nextToken(json, index); + + // Loop through all of the key/value pairs of the object + bool done = false; + while (!done) { + // Get the upcoming token + token = lookAhead(json, index); + + if (token == JsonTokenNone) { + success = false; + return QVariantMap(); + } else if (token == JsonTokenComma) { + nextToken(json, index); + } else if (token == JsonTokenCurlyClose) { + nextToken(json, index); + return map; + } else { + // Parse the key/value pair's name + QString name = parseString(json, index, success).toString(); + + if (!success) { + return QVariantMap(); + } + + // Get the next token + token = nextToken(json, index); + + // If the next token is not a colon, flag the failure + // return an empty QVariant + if (token != JsonTokenColon) { + success = false; + return QVariant(QVariantMap()); + } + + // Parse the key/value pair's value + QVariant value = parseValue(json, index, success); + + if (!success) { + return QVariantMap(); + } + + // Assign the value to the key in the map + map[name] = value; + } + } + + // Return the map successfully + return QVariant(map); +} + +/** + * parseArray + */ +static QVariant parseArray(const QString& json, int& index, bool& success) +{ + QVariantList list; + + nextToken(json, index); + + bool done = false; + while (!done) { + int token = lookAhead(json, index); + + if (token == JsonTokenNone) { + success = false; + return QVariantList(); + } else if (token == JsonTokenComma) { + nextToken(json, index); + } else if (token == JsonTokenSquaredClose) { + nextToken(json, index); + break; + } else { + QVariant value = parseValue(json, index, success); + if (!success) { + return QVariantList(); + } + list.push_back(value); + } + } + + return QVariant(list); +} + +/** + * parseString + */ +static QVariant parseString(const QString& json, int& index, bool& success) +{ + QString s; + QChar c; + + eatWhitespace(json, index); + + c = json[index++]; + + bool complete = false; + while (!complete) { + if (index == json.size()) { + break; + } + + c = json[index++]; + + if (c == '\"') { + complete = true; + break; + } else if (c == '\\') { + if (index == json.size()) { + break; + } + + c = json[index++]; + + if (c == '\"') { + s.append('\"'); + } else if (c == '\\') { + s.append('\\'); + } else if (c == '/') { + s.append('/'); + } else if (c == 'b') { + s.append('\b'); + } else if (c == 'f') { + s.append('\f'); + } else if (c == 'n') { + s.append('\n'); + } else if (c == 'r') { + s.append('\r'); + } else if (c == 't') { + s.append('\t'); + } else if (c == 'u') { + qsizetype remainingLength = json.size() - index; + if (remainingLength >= 4) { + QString unicodeStr = json.mid(index, 4); + + int symbol = unicodeStr.toInt(0, 16); + + s.append(QChar(symbol)); + + index += 4; + } else { + break; + } + } + } else { + s.append(c); + } + } + + if (!complete) { + success = false; + return QVariant(); + } + + return QVariant(s); +} + +/** + * parseNumber + */ +static QVariant parseNumber(const QString& json, int& index) +{ + eatWhitespace(json, index); + + int lastIndex = lastIndexOfNumber(json, index); + int charLength = (lastIndex - index) + 1; + QString numberStr; + + numberStr = json.mid(index, charLength); + + index = lastIndex + 1; + bool ok; + + if (numberStr.contains('.')) { + return QVariant(numberStr.toDouble(nullptr)); + } else if (numberStr.startsWith('-')) { + int i = numberStr.toInt(&ok); + if (!ok) { + qlonglong ll = numberStr.toLongLong(&ok); + return ok ? ll : QVariant(numberStr); + } + return i; + } else { + uint u = numberStr.toUInt(&ok); + if (!ok) { + qulonglong ull = numberStr.toULongLong(&ok); + return ok ? ull : QVariant(numberStr); + } + return u; + } +} + +/** + * lastIndexOfNumber + */ +static int lastIndexOfNumber(const QString& json, int index) +{ + int lastIndex; + + for (lastIndex = index; lastIndex < json.size(); lastIndex++) { + if (QString("0123456789+-.eE").indexOf(json[lastIndex]) == -1) { + break; + } + } + + return lastIndex - 1; +} + +/** + * eatWhitespace + */ +static void eatWhitespace(const QString& json, int& index) +{ + for (; index < json.size(); index++) { + if (QString(" \t\n\r").indexOf(json[index]) == -1) { + break; + } + } +} + +/** + * lookAhead + */ +static int lookAhead(const QString& json, int index) +{ + int saveIndex = index; + return nextToken(json, saveIndex); +} + +/** + * nextToken + */ +static int nextToken(const QString& json, int& index) +{ + eatWhitespace(json, index); + + if (index == json.size()) { + return JsonTokenNone; + } + + QChar c = json[index]; + index++; + switch (c.toLatin1()) { + case '{': + return JsonTokenCurlyOpen; + case '}': + return JsonTokenCurlyClose; + case '[': + return JsonTokenSquaredOpen; + case ']': + return JsonTokenSquaredClose; + case ',': + return JsonTokenComma; + case '"': + return JsonTokenString; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + return JsonTokenNumber; + case ':': + return JsonTokenColon; + } + index--; // ^ WTF? + + qsizetype remainingLength = json.size() - index; + + // True + if (remainingLength >= 4) { + if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && + json[index + 3] == 'e') { + index += 4; + return JsonTokenTrue; + } + } + + // False + if (remainingLength >= 5) { + if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && + json[index + 3] == 's' && json[index + 4] == 'e') { + index += 5; + return JsonTokenFalse; + } + } + + // Null + if (remainingLength >= 4) { + if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && + json[index + 3] == 'l') { + index += 4; + return JsonTokenNull; + } + } + + return JsonTokenNone; +} +} // namespace QtJson diff --git a/libs/uibase/src/lineeditclear.cpp b/libs/uibase/src/lineeditclear.cpp new file mode 100644 index 0000000..476c874 --- /dev/null +++ b/libs/uibase/src/lineeditclear.cpp @@ -0,0 +1,50 @@ +/**************************************************************************** +** +** Copyright (c) 2007 Trolltech ASA +** +** Use, modification and distribution is allowed without limitation, +** warranty, liability or support of any kind. +** +****************************************************************************/ + +#include +#include +#include + +namespace MOBase +{ + +LineEditClear::LineEditClear(QWidget* parent) : QLineEdit(parent) +{ + clearButton = new QToolButton(this); + QPixmap pixmap(":/MO/gui/edit_clear"); + clearButton->setIcon(QIcon(pixmap)); + clearButton->setIconSize(pixmap.size()); + clearButton->setCursor(Qt::ArrowCursor); + clearButton->setStyleSheet("QToolButton { border: none; padding: 0px; }"); + clearButton->hide(); + connect(clearButton, SIGNAL(clicked()), this, SLOT(clear())); + connect(this, SIGNAL(textChanged(const QString&)), this, + SLOT(updateCloseButton(const QString&))); + int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); + setStyleSheet(QString("QLineEdit { padding-right: %1px; } ") + .arg(clearButton->sizeHint().width() + frameWidth + 1)); + /* QSize msz = minimumSizeHint(); + setMinimumSize(qMax(msz.width(), clearButton->sizeHint().height() + frameWidth * 2 + + 2), qMax(msz.height(), clearButton->sizeHint().height() + frameWidth * 2 + 2));*/ +} + +void LineEditClear::resizeEvent(QResizeEvent*) +{ + QSize sz = clearButton->sizeHint(); + int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); + clearButton->move(rect().right() - frameWidth - sz.width(), + (rect().bottom() + 1 - sz.height()) / 2); +} + +void LineEditClear::updateCloseButton(const QString& text) +{ + clearButton->setVisible(!text.isEmpty()); +} + +} // namespace MOBase diff --git a/libs/uibase/src/linklabel.cpp b/libs/uibase/src/linklabel.cpp new file mode 100644 index 0000000..c4d09f1 --- /dev/null +++ b/libs/uibase/src/linklabel.cpp @@ -0,0 +1,27 @@ +#include +#include + +QColor LinkLabel::m_linkColor; + +LinkLabel::LinkLabel(QWidget* parent) : QLabel(parent) {} + +QColor LinkLabel::linkColor() const +{ + return m_linkColor; +} + +void LinkLabel::setLinkColor(const QColor& c) +{ + if (m_linkColor != c) { + m_linkColor = c; + + // setting link color on the global palette; qt doesn't seem to support + // per-widget colors for links + if (qApp) { + auto p = qApp->palette(); + p.setColor(QPalette::Link, c); + p.setColor(QPalette::LinkVisited, c); + qApp->setPalette(p); + } + } +} diff --git a/libs/uibase/src/log.cpp b/libs/uibase/src/log.cpp new file mode 100644 index 0000000..a44c9e3 --- /dev/null +++ b/libs/uibase/src/log.cpp @@ -0,0 +1,411 @@ +#include +#include "pch.h" +#include +#include + +#include +#include + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4365) +#endif + +#ifdef _WIN32 +#define SPDLOG_WCHAR_FILENAMES 1 +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +namespace MOBase::log +{ + +namespace fs = std::filesystem; +static std::unique_ptr g_default; + +spdlog::level::level_enum toSpdlog(Levels lv) +{ + switch (lv) { + case Debug: + return spdlog::level::debug; + + case Warning: + return spdlog::level::warn; + + case Error: + return spdlog::level::err; + + case Info: // fall-through + default: + return spdlog::level::info; + } +} + +Levels fromSpdlog(spdlog::level::level_enum lv) +{ + switch (lv) { + case spdlog::level::trace: + case spdlog::level::debug: + return Debug; + + case spdlog::level::warn: + return Warning; + + case spdlog::level::critical: // fall-through + case spdlog::level::err: + return Error; + + case spdlog::level::info: // fall-through + case spdlog::level::off: + case spdlog::level::n_levels: // to please MSVC + default: + return Info; + } +} + +class CallbackSink : public spdlog::sinks::base_sink +{ +public: + CallbackSink(Callback* f) : m_f(f) {} + + void setCallback(Callback* f) { m_f = f; } + +protected: + void sink_it_(const spdlog::details::log_msg& m) override + { + thread_local bool active = false; + + if (active) { + // trying to log from a log callback, ignoring + return; + } + + if (!m_f) { + // disabled + return; + } + + try { + auto g = Guard([&] { + active = false; + }); + active = true; + + Entry e; + e.time = m.time; + e.level = fromSpdlog(m.level); + e.message = std::string(m.payload); + + spdlog::memory_buf_t formatted; + base_sink::formatter_->format(m, formatted); + + if (formatted.size() >= 2) { + // remove \r\n + e.formattedMessage.assign(formatted.begin(), formatted.end() - 2); + } else { + e.formattedMessage = std::string(formatted); + } + + (*m_f)(std::move(e)); + } catch (std::exception& e) { + fprintf(stderr, "uncaugh exception in logging callback, %s\n", e.what()); + } catch (...) { + fprintf(stderr, "uncaught exception in logging callback\n"); + } + } + + void flush_() override + { + // no-op + } + +private: + std::atomic m_f; +}; + +File::File() : type(None), maxSize(0), maxFiles(0), dailyHour(0), dailyMinute(0) {} + +File File::daily(fs::path file, int hour, int minute) +{ + File fl; + + fl.type = Daily; + fl.file = std::move(file); + fl.dailyHour = hour; + fl.dailyMinute = minute; + + return fl; +} + +File File::rotating(fs::path file, std::size_t maxSize, std::size_t maxFiles) +{ + File fl; + + fl.type = Rotating; + fl.file = std::move(file); + fl.maxSize = maxSize; + fl.maxFiles = maxFiles; + + return fl; +} + +File File::single(std::filesystem::path file) +{ + File fl; + + fl.type = Single; + fl.file = std::move(file); + + return fl; +} + +spdlog::sink_ptr createFileSink(const File& f) +{ + try { + switch (f.type) { + case File::Daily: { + return std::make_shared( + f.file.native(), f.dailyHour, f.dailyMinute); + } + + case File::Rotating: { + return std::make_shared( + f.file.native(), f.maxSize, f.maxFiles); + } + + case File::Single: { + return std::make_shared(f.file.native(), true); + } + + case File::None: // fall-through + default: + return {}; + } + } catch (spdlog::spdlog_ex& e) { + std::cerr << "failed to create file log, " << e.what() << "\n"; + return {}; + } +} + +Logger::Logger(LoggerConfiguration conf_moved) : m_conf(std::move(conf_moved)) +{ + createLogger(m_conf.name); + + const auto timeType = + m_conf.utc ? spdlog::pattern_time_type::utc : spdlog::pattern_time_type::local; + + m_logger->set_level(toSpdlog(m_conf.maxLevel)); + m_logger->set_pattern(m_conf.pattern, timeType); + m_logger->flush_on(spdlog::level::trace); +} + +// anchor +Logger::~Logger() = default; + +Levels Logger::level() const +{ + return fromSpdlog(m_logger->level()); +} + +void Logger::setLevel(Levels lv) +{ + m_logger->set_level(toSpdlog(lv)); +} + +void Logger::setPattern(const std::string& s) +{ + m_logger->set_pattern(s); +} + +void Logger::setFile(const File& f) +{ + + if (m_file) { + auto* ds = static_cast*>(m_sinks.get()); + ds->remove_sink(m_file); + m_file = {}; + } + + if (f.type != File::None) { + try { + m_file = createFileSink(f); + + if (m_file) { + addSink(m_file); + } + } catch (spdlog::spdlog_ex& e) { + error("{}", e.what()); + } + } +} + +void Logger::setCallback(Callback* f) +{ + if (m_callback) { + static_cast(m_callback.get())->setCallback(f); + } else { + m_callback.reset(new CallbackSink(f)); + addSink(m_callback); + } +} + +void Logger::addToBlacklist(const std::string& filter, const std::string& replacement) +{ + if (filter.length() <= 0 || replacement.length() <= 0) { + // nothing to do + return; + } + + bool present = false; + for (BlacklistEntry& e : m_conf.blacklist) { + if (iequals(e.filter, filter)) { + e.replacement = replacement; + present = true; + break; + } + } + if (!present) { + m_conf.blacklist.push_back(BlacklistEntry(filter, replacement)); + } +} + +void Logger::removeFromBlacklist(const std::string& filter) +{ + if (filter.length() <= 0) { + // nothing to do + return; + } + + for (auto it = m_conf.blacklist.begin(); it != m_conf.blacklist.end();) { + if (iequals(it->filter, filter)) { + it = m_conf.blacklist.erase(it); + } else { + ++it; + } + } +} + +void Logger::resetBlacklist() +{ + m_conf.blacklist.clear(); +} + +void Logger::createLogger(const std::string& name) +{ + m_sinks.reset(new spdlog::sinks::dist_sink); + +#ifdef _WIN32 + DWORD console_mode; + if (::GetConsoleMode(::GetStdHandle(STD_ERROR_HANDLE), &console_mode) != 0) { + using sink_type = spdlog::sinks::wincolor_stderr_sink_mt; + m_console.reset(new sink_type); + + if (auto* cs = dynamic_cast(m_console.get())) { + cs->set_color(spdlog::level::info, + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + cs->set_color(spdlog::level::debug, + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + } + + addSink(m_console); + } +#else + // On Linux, use ANSI color stderr sink + m_console.reset(new spdlog::sinks::ansicolor_stderr_sink_mt); + addSink(m_console); +#endif + + m_logger.reset(new spdlog::logger(name, m_sinks)); +} + +void Logger::addSink(std::shared_ptr sink) +{ + // this is called for both the file and callback sinks + // + // in createLogger(), the dist_sink that was just created will be given the + // pattern that was set in Logger::Logger(), and will pass it to its children; + // the log level is irrelevant in child sinks because dist_sink checks it + // itself + // + // the problem then is that dist_sink doesn't have children yet, they're added + // in setFile() and setCallback(), which can be called by the user much later + // (or not at all) + // + // however, when a sink is added to dist_sink, it does _not_ set the pattern + // on it, it merely adds it to the list + // + // this sets the formatter on the sink manually before adding it to dist_sink + + auto* ds = static_cast*>(m_sinks.get()); + + const auto timeType = + m_conf.utc ? spdlog::pattern_time_type::utc : spdlog::pattern_time_type::local; + + sink->set_formatter( + std::make_unique(m_conf.pattern, timeType)); + + ds->add_sink(sink); +} + +QString levelToString(Levels level) +{ + const auto spdlogLevel = toSpdlog(level); + const auto sv = spdlog::level::to_string_view(spdlogLevel); + const std::string s(sv.begin(), sv.end()); + + return QString::fromStdString(s); +} + +void createDefault(LoggerConfiguration conf) +{ + g_default = std::make_unique(conf); +} + +Logger& getDefault() +{ + Q_ASSERT(g_default); + return *g_default; +} + +} // namespace MOBase::log + +namespace MOBase::log::details +{ + +void doLogImpl(spdlog::logger& lg, Levels lv, const std::string& s) noexcept +{ + try { + const char* start = s.c_str(); + const char* p = start; + + for (;;) { + while (*p && *p != '\n') { + ++p; + } + + std::string_view sv(start, static_cast(p - start)); + lg.log(toSpdlog(lv), "{}", sv); + + if (!*p) { + break; + } + + ++p; + start = p; + } + } catch (...) { + // eat it + } +} + +} // namespace MOBase::log::details diff --git a/libs/uibase/src/modrepositoryfileinfo.cpp b/libs/uibase/src/modrepositoryfileinfo.cpp new file mode 100644 index 0000000..2b0f729 --- /dev/null +++ b/libs/uibase/src/modrepositoryfileinfo.cpp @@ -0,0 +1,81 @@ +#include +#include + +MOBase::ModRepositoryFileInfo::ModRepositoryFileInfo( + const ModRepositoryFileInfo& reference) + : QObject(reference.parent()), name(reference.name), uri(reference.uri), + description(reference.description), version(reference.version), + categoryID(reference.categoryID), modName(reference.modName), + gameName(reference.gameName), modID(reference.modID), fileID(reference.fileID), + fileSize(reference.fileSize), fileCategory(reference.fileCategory), + repository(reference.repository), userData(reference.userData), + author(reference.author), uploader(reference.uploader), + uploaderUrl(reference.uploaderUrl) +{} + +MOBase::ModRepositoryFileInfo::ModRepositoryFileInfo(QString gameName, int modID, + int fileID) + : name(), uri(), description(), version(), categoryID(0), modName(), + gameName(gameName), modID(modID), fileID(fileID), fileSize(0), + fileCategory(TYPE_UNKNOWN), repository(), userData(), author(), uploader(), + uploaderUrl() + +{} + +MOBase::ModRepositoryFileInfo +MOBase::ModRepositoryFileInfo::createFromJson(const QString& data) +{ + QVariantList result = QtJson::parse(data).toList(); + + while (result.length() < 18) { + result.append(QVariant()); + } + + ModRepositoryFileInfo newInfo; + + newInfo.gameName = result.at(0).toString(); + newInfo.fileID = result.at(1).toInt(); + newInfo.name = result.at(2).toString(); + newInfo.uri = result.at(3).toString(); + newInfo.version.parse(result.at(4).toString()); + newInfo.description = result.at(5).toString(); + newInfo.categoryID = result.at(6).toInt(); + newInfo.fileSize = result.at(7).toUInt(); + newInfo.modID = result.at(8).toInt(); + newInfo.modName = result.at(9).toString(); + newInfo.newestVersion.parse(result.at(10).toString()); + newInfo.fileName = result.at(11).toString(); + newInfo.fileCategory = result.at(12).toInt(); + newInfo.repository = result.at(13).toString(); + newInfo.userData = result.at(14).toMap(); + newInfo.author = result.at(15).toString(); + newInfo.uploader = result.at(16).toString(); + newInfo.uploaderUrl = result.at(17).toString(); + + return newInfo; +} + +QString MOBase::ModRepositoryFileInfo::toString() const +{ + return QString("[ " + "\"%1\",%2,\"%3\",\"%4\",\"%5\",\"%6\",%7,%8,%9,\"%10\",\"%11\",\"%" + "12\",%13,\"%14\",%15 ]") + .arg(gameName) + .arg(fileID) + .arg(name) + .arg(uri) + .arg(version.canonicalString()) + .arg(description.mid(0).replace("\"", "'")) + .arg(categoryID) + .arg(fileSize) + .arg(modID) + .arg(modName) + .arg(newestVersion.canonicalString()) + .arg(fileName) + .arg(fileCategory) + .arg(repository) + .arg(QString(QtJson::serialize(userData))) + .arg(author) + .arg(uploader) + .arg(uploaderUrl); +} diff --git a/libs/uibase/src/nxmurl.cpp b/libs/uibase/src/nxmurl.cpp new file mode 100644 index 0000000..d09cdde --- /dev/null +++ b/libs/uibase/src/nxmurl.cpp @@ -0,0 +1,44 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include +#include +#include +#include +#include +#include +#include + +NXMUrl::NXMUrl(const QString& url) +{ + QUrl nxm(url); + QUrlQuery query(nxm); + QRegularExpression exp("nxm://[a-z0-9]+/mods/(\\d+)/files/(\\d+)", + QRegularExpression::CaseInsensitiveOption); + auto match = exp.match(url); + if (!match.hasMatch()) { + throw MOBase::InvalidNXMLinkException(url); + } + m_Game = nxm.host(); + m_ModId = match.captured(1).toInt(); + m_FileId = match.captured(2).toInt(); + m_Key = query.queryItemValue("key"); + m_Expires = query.queryItemValue("expires").toInt(); + m_UserId = query.queryItemValue("user_id").toInt(); +} diff --git a/libs/uibase/src/pch.cpp b/libs/uibase/src/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/libs/uibase/src/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/libs/uibase/src/pch.h b/libs/uibase/src/pch.h new file mode 100644 index 0000000..bd7e3bc --- /dev/null +++ b/libs/uibase/src/pch.h @@ -0,0 +1,139 @@ +#ifdef _MSC_VER +#pragma warning(disable : 4251) // neds to have dll-interface +#pragma warning(disable : 4355) // this used in initializer list +#pragma warning(disable : 4371) // layout may have changed +#pragma warning(disable : 4514) // unreferenced inline function removed +#pragma warning(disable : 4571) // catch semantics changed +#pragma warning(disable : 4619) // no warning X +#pragma warning(disable : 4623) // default constructor deleted +#pragma warning(disable : 4625) // copy constructor deleted +#pragma warning(disable : 4626) // copy assignment operator deleted +#pragma warning(disable : 4710) // function not inlined +#pragma warning(disable : 4820) // padding +#pragma warning(disable : 4866) // left-to-right evaluation order +#pragma warning(disable : 4868) // left-to-right evaluation order +#pragma warning(disable : 5026) // move constructor deleted +#pragma warning(disable : 5027) // move assignment operator deleted +#pragma warning(disable : 5045) // spectre mitigation + +#pragma warning(push, 3) +#pragma warning(disable : 4242) // uint -> const quint8 +#pragma warning(disable : 4365) // signed/unsigned mismatch +#pragma warning(disable : 4668) // preprocessor macro used but not defined +#pragma warning(disable : 4774) // bad format string +#pragma warning(disable : 4946) // reinterpret_cast used between related classes +#pragma warning(disable : 4800) // implicit conversion +#pragma warning(disable : 5219) // implicit int -> float conversion +#pragma warning( \ + disable : 5249) // named enumerators with values outside of bit field width +#endif // _MSC_VER + +#define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING 1 + +// std +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// windows +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_MEAN_AND_LEAN +#define WIN32_MEAN_AND_LEAN +#endif +#include +#include +#include +#endif // _WIN32 + +// Qt +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING +#ifdef _MSC_VER +#pragma warning(pop) +#endif diff --git a/libs/uibase/src/pluginrequirements.cpp b/libs/uibase/src/pluginrequirements.cpp new file mode 100644 index 0000000..a2a11ef --- /dev/null +++ b/libs/uibase/src/pluginrequirements.cpp @@ -0,0 +1,138 @@ +#include + +#include + +#include +#include +#include + +using namespace MOBase; + +// Plugin and Game dependencies + +PluginDependencyRequirement::PluginDependencyRequirement(QStringList const& pluginNames) + : m_PluginNames(pluginNames) +{} + +std::optional +PluginDependencyRequirement::check(IOrganizer* o) const +{ + for (auto const& pluginName : m_PluginNames) { + if (o->isPluginEnabled(pluginName)) { + return {}; + } + } + return Problem(message()); +} + +QString PluginDependencyRequirement::message() const +{ + if (m_PluginNames.size() > 1) { + return QObject::tr("One of the following plugins must be enabled: %1.") + .arg(m_PluginNames.join(", ")); + } else { + return QObject::tr("This plugin can only be enabled if the '%1' plugin is " + "installed and enabled.") + .arg(m_PluginNames[0]); + } +} + +GameDependencyRequirement::GameDependencyRequirement(QStringList const& gameNames) + : m_GameNames(gameNames) +{} + +std::optional +GameDependencyRequirement::check(IOrganizer* o) const +{ + auto* game = o->managedGame(); + if (!game) { + return Problem(message()); + } + + QString gameName = game->gameName(); + for (auto const& pluginName : m_GameNames) { + if (pluginName.compare(gameName, Qt::CaseInsensitive) == 0) { + return {}; + } + } + return Problem(message()); +} + +QString GameDependencyRequirement::message() const +{ + return QObject::tr("This plugin can only be enabled for the following game(s): %1.", + "", static_cast(m_GameNames.size())) + .arg(m_GameNames.join(", ")); +} + +// Diagnose requirements + +DiagnoseRequirement::DiagnoseRequirement(const IPluginDiagnose* diagnose) + : m_Diagnose(diagnose) +{} + +std::optional DiagnoseRequirement::check(IOrganizer*) const +{ + auto activeProblems = m_Diagnose->activeProblems(); + + if (activeProblems.empty()) { + return {}; + } + + QStringList shortDescriptions, longDescriptions; + for (auto i : activeProblems) { + shortDescriptions.append(m_Diagnose->shortDescription(i)); + longDescriptions.append(m_Diagnose->fullDescription(i)); + } + + return Problem(shortDescriptions.join("\n"), longDescriptions.join("\n")); +} + +// Basic requirements +class BasicPluginRequirement : public IPluginRequirement +{ +public: + BasicPluginRequirement(std::function const& checker, + QString const description) + : m_Checker(checker), m_Description(description) + {} + + std::optional check(IOrganizer* o) const + { + if (m_Checker(o)) { + return {}; + } + return Problem(m_Description); + } + +private: + std::function m_Checker; + QString m_Description; +}; + +// Factory + +std::shared_ptr +PluginRequirementFactory::pluginDependency(QStringList const& pluginNames) +{ + return std::make_shared(pluginNames); +} + +std::shared_ptr +PluginRequirementFactory::gameDependency(QStringList const& pluginGameNames) +{ + return std::make_shared(pluginGameNames); +} + +std::shared_ptr +PluginRequirementFactory::diagnose(const IPluginDiagnose* diagnose) +{ + return std::make_shared(diagnose); +} + +std::shared_ptr +PluginRequirementFactory::basic(std::function const& checker, + QString const description) +{ + return std::make_shared(checker, description); +} diff --git a/libs/uibase/src/questionboxmemory.cpp b/libs/uibase/src/questionboxmemory.cpp new file mode 100644 index 0000000..586e52e --- /dev/null +++ b/libs/uibase/src/questionboxmemory.cpp @@ -0,0 +1,200 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 +#include +#include "ui_questionboxmemory.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace MOBase +{ + +static QMutex g_mutex; +static QuestionBoxMemory::GetButton g_get; +static QuestionBoxMemory::SetWindowButton g_setWindow; +static QuestionBoxMemory::SetFileButton g_setFile; + +QuestionBoxMemory::QuestionBoxMemory(QWidget* parent, const QString& title, + const QString& text, QString const* filename, + const QDialogButtonBox::StandardButtons buttons, + QDialogButtonBox::StandardButton defaultButton) + : QDialog(parent), ui(new Ui::QuestionBoxMemory), m_Button(QDialogButtonBox::Cancel) +{ + ui->setupUi(this); + + setWindowFlag(Qt::WindowType::WindowContextHelpButtonHint, false); + setWindowTitle(title); + + QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion); + ui->iconLabel->setPixmap(icon.pixmap(128)); + ui->messageLabel->setText(text); + + if (filename == nullptr) { + // delete the 2nd check box + QCheckBox* box = ui->rememberForCheckBox; + box->parentWidget()->layout()->removeWidget(box); + delete box; + } else { + ui->rememberForCheckBox->setText(ui->rememberForCheckBox->text().arg(*filename)); + } + + ui->buttonBox->setStandardButtons(buttons); + + if (defaultButton != QDialogButtonBox::NoButton) { + ui->buttonBox->button(defaultButton)->setDefault(true); + } + + connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this, + SLOT(buttonClicked(QAbstractButton*))); +} + +QuestionBoxMemory::~QuestionBoxMemory() = default; + +void QuestionBoxMemory::setCallbacks(GetButton get, SetWindowButton setWindow, + SetFileButton setFile) +{ + QMutexLocker locker(&g_mutex); + + g_get = get; + g_setWindow = setWindow; + g_setFile = setFile; +} + +void QuestionBoxMemory::buttonClicked(QAbstractButton* button) +{ + m_Button = ui->buttonBox->standardButton(button); +} + +QDialogButtonBox::StandardButton +QuestionBoxMemory::query(QWidget* parent, const QString& windowName, + const QString& title, const QString& text, + QDialogButtonBox::StandardButtons buttons, + QDialogButtonBox::StandardButton defaultButton) +{ + return queryImpl(parent, windowName, nullptr, title, text, buttons, defaultButton); +} + +QDialogButtonBox::StandardButton +QuestionBoxMemory::query(QWidget* parent, const QString& windowName, + const QString& fileName, const QString& title, + const QString& text, QDialogButtonBox::StandardButtons buttons, + QDialogButtonBox::StandardButton defaultButton) +{ + return queryImpl(parent, windowName, &fileName, title, text, buttons, defaultButton); +} + +QDialogButtonBox::StandardButton +QuestionBoxMemory::queryImpl(QWidget* parent, const QString& windowName, + const QString* fileName, const QString& title, + const QString& text, + QDialogButtonBox::StandardButtons buttons, + QDialogButtonBox::StandardButton defaultButton) +{ + QMutexLocker locker(&g_mutex); + + const auto button = getMemory(windowName, (fileName ? *fileName : "")); + if (button != NoButton) { + log::debug("{}: not asking because user always wants response {}", + windowName + (fileName ? QString("/") + *fileName : ""), + buttonToString(button)); + + return button; + } + + QuestionBoxMemory dialog(parent, title, text, fileName, buttons, defaultButton); + dialog.exec(); + + if (dialog.m_Button != QDialogButtonBox::Cancel) { + if (dialog.ui->rememberCheckBox->isChecked()) { + setWindowMemory(windowName, dialog.m_Button); + } + + if (fileName != nullptr && dialog.ui->rememberForCheckBox->isChecked()) { + setFileMemory(windowName, *fileName, dialog.m_Button); + } + } + + return dialog.m_Button; +} + +void QuestionBoxMemory::setWindowMemory(const QString& windowName, Button b) +{ + log::debug("remembering choice {} for window {}", buttonToString(b), windowName); + + g_setWindow(windowName, b); +} + +void QuestionBoxMemory::setFileMemory(const QString& windowName, + const QString& filename, Button b) +{ + log::debug("remembering choice {} for file {}", buttonToString(b), + windowName + "/" + filename); + + g_setFile(windowName, filename, b); +} + +QuestionBoxMemory::Button QuestionBoxMemory::getMemory(const QString& windowName, + const QString& filename) +{ + return g_get(windowName, filename); +} + +QString QuestionBoxMemory::buttonToString(Button b) +{ + using BB = QDialogButtonBox; + + static const std::map map = { + {BB::NoButton, "none"}, + {BB::Ok, "ok"}, + {BB::Save, "save"}, + {BB::SaveAll, "saveall"}, + {BB::Open, "open"}, + {BB::Yes, "yes"}, + {BB::YesToAll, "yestoall"}, + {BB::No, "no"}, + {BB::NoToAll, "notoall"}, + {BB::Abort, "abort"}, + {BB::Retry, "retry"}, + {BB::Ignore, "ignore"}, + {BB::Close, "close"}, + {BB::Cancel, "cancel"}, + {BB::Discard, "discard"}, + {BB::Help, "help"}, + {BB::Apply, "apply"}, + {BB::Reset, "reset"}, + {BB::RestoreDefaults, "restoredefaults"}}; + + auto itor = map.find(b); + + if (itor == map.end()) { + return QString("0x%1").arg(static_cast(b), 0, 16); + } else { + return QString("'%1' (0x%2)").arg(itor->second).arg(static_cast(b), 0, 16); + } +} + +} // namespace MOBase diff --git a/libs/uibase/src/questionboxmemory.ui b/libs/uibase/src/questionboxmemory.ui new file mode 100644 index 0000000..8237571 --- /dev/null +++ b/libs/uibase/src/questionboxmemory.ui @@ -0,0 +1,171 @@ + + + QuestionBoxMemory + + + + 0 + 0 + 374 + 130 + + + + Placeholder + + + + 1 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 36 + 36 + + + + false + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + 9 + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + + 7 + + + 7 + + + 7 + + + 7 + + + + + + + Remember selection + + + + + + + Remember selection only for %1 + + + + + + + + + true + + + Qt::Horizontal + + + QDialogButtonBox::No|QDialogButtonBox::Yes + + + + + + + + + + + buttonBox + accepted() + QuestionBoxMemory + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + QuestionBoxMemory + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/libs/uibase/src/registry.cpp b/libs/uibase/src/registry.cpp new file mode 100644 index 0000000..0b6834f --- /dev/null +++ b/libs/uibase/src/registry.cpp @@ -0,0 +1,189 @@ +/* +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MOBase +{ + +// Line-by-line INI writer that preserves the file format. +// Unlike QSettings::IniFormat, this does NOT interpret backslashes as +// line continuations, does NOT URL-encode spaces in key names, and does +// NOT reorder keys. It only modifies the target key=value pair and +// leaves everything else untouched. +static bool writeIniValueDirect(const QString& section, const QString& key, + const QString& value, const QString& fileName) +{ + QStringList lines; + bool fileExists = QFileInfo::exists(fileName); + + if (fileExists) { + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return false; + } + QTextStream in(&file); + while (!in.atEnd()) { + lines.append(in.readLine()); + } + file.close(); + } + + // Find the target section and key + QString sectionHeader = "[" + section + "]"; + int sectionStart = -1; + int sectionEnd = lines.size(); // end of file if section is last + int keyLine = -1; + + for (int i = 0; i < lines.size(); ++i) { + QString trimmed = lines[i].trimmed(); + if (trimmed.compare(sectionHeader, Qt::CaseInsensitive) == 0) { + sectionStart = i; + // Find end of this section (next section header or EOF) + for (int j = i + 1; j < lines.size(); ++j) { + QString t = lines[j].trimmed(); + if (t.startsWith('[') && t.endsWith(']')) { + sectionEnd = j; + break; + } + } + break; + } + } + + if (sectionStart >= 0) { + // Section found, look for the key within it + for (int i = sectionStart + 1; i < sectionEnd; ++i) { + QString trimmed = lines[i].trimmed(); + // Skip comments and empty lines + if (trimmed.isEmpty() || trimmed.startsWith(';') || trimmed.startsWith('#')) { + continue; + } + int eqPos = trimmed.indexOf('='); + if (eqPos > 0) { + QString existingKey = trimmed.left(eqPos).trimmed(); + if (existingKey.compare(key, Qt::CaseInsensitive) == 0) { + keyLine = i; + break; + } + } + } + + if (keyLine >= 0) { + // Key found, replace the line preserving indentation + QString original = lines[keyLine]; + int eqPos = original.indexOf('='); + // Preserve everything up to and including '=' + lines[keyLine] = original.left(eqPos + 1) + value; + } else { + // Key not found in section, insert it after the section header + lines.insert(sectionStart + 1, key + "=" + value); + } + } else { + // Section not found, append it + if (!lines.isEmpty() && !lines.last().trimmed().isEmpty()) { + lines.append(""); // blank line before new section + } + lines.append(sectionHeader); + lines.append(key + "=" + value); + } + + // Write back + QFile outFile(fileName); + if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + return false; + } + QTextStream out(&outFile); + for (int i = 0; i < lines.size(); ++i) { + out << lines[i]; + if (i < lines.size() - 1) { + out << '\n'; + } + } + // Preserve trailing newline if original had one, or add one + out << '\n'; + outFile.close(); + + return true; +} + +bool WriteRegistryValue(const QString& appName, const QString& keyName, + const QString& value, const QString& fileName) +{ + if (writeIniValueDirect(appName, keyName, value, fileName)) { + return true; + } + + // Write failed, check if the file is read-only + QFileInfo fileInfo(fileName); + + QMessageBox::StandardButton result = + MOBase::TaskDialog(qApp->activeModalWidget(), + QObject::tr("INI file is read-only")) + .main(QObject::tr("INI file is read-only")) + .content(QObject::tr("Mod Organizer is attempting to write to \"%1\" " + "which is currently set to read-only.") + .arg(fileInfo.fileName())) + .icon(QMessageBox::Warning) + .button({QObject::tr("Clear the read-only flag"), QMessageBox::Yes}) + .button({QObject::tr("Allow the write once"), + QObject::tr("The file will be set to read-only again."), + QMessageBox::Ignore}) + .button({QObject::tr("Skip this file"), QMessageBox::No}) + .remember("clearReadOnly", fileInfo.fileName()) + .exec(); + + if (result & (QMessageBox::Yes | QMessageBox::Ignore)) { + // Make the file writable + QFile file(fileName); + file.setPermissions(file.permissions() | QFile::WriteUser | QFile::WriteOwner); + + bool ok = writeIniValueDirect(appName, keyName, value, fileName); + + if (result == QMessageBox::Ignore) { + // Set back to read-only + file.setPermissions(file.permissions() & ~(QFile::WriteUser | QFile::WriteOwner)); + } + + return ok; + } + + return false; +} + +#ifdef _WIN32 +bool WriteRegistryValue(const wchar_t* appName, const wchar_t* keyName, + const wchar_t* value, const wchar_t* fileName) +{ + return WriteRegistryValue( + QString::fromWCharArray(appName), + QString::fromWCharArray(keyName), + QString::fromWCharArray(value), + QString::fromWCharArray(fileName)); +} +#endif + +} // namespace MOBase diff --git a/libs/uibase/src/report.cpp b/libs/uibase/src/report.cpp new file mode 100644 index 0000000..1c47d95 --- /dev/null +++ b/libs/uibase/src/report.cpp @@ -0,0 +1,453 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 +#include +#include +#include +#include "ui_taskdialog.h" +#include +#include +#include +#include +#include + +namespace MOBase +{ + +QWidget* topLevelWindow() +{ + if (!qApp) { + return nullptr; + } + + for (QWidget* w : qApp->topLevelWidgets()) { + if (dynamic_cast(w)) { + return w; + } + } + + return nullptr; +} + +void criticalOnTop(const QString& message) +{ + QMessageBox mb(QMessageBox::Critical, "Mod Organizer", message); + + mb.show(); + mb.activateWindow(); + mb.raise(); + mb.exec(); +} + +void reportError(const QString& message) +{ + log::error("{}", message); + + // QMessageBox must only be created on the main (GUI) thread. + // If we're on a background thread, just log — showing a dialog would crash. + if (QThread::currentThread() != qApp->thread()) { + return; + } + + if (QApplication::topLevelWidgets().count() != 0) { + if (auto* mw = topLevelWindow()) { + QMessageBox::warning(mw, QObject::tr("Error"), message, QMessageBox::Ok); + } else { + criticalOnTop(message); + } + } else { + // No top-level widget available, show a standalone critical dialog + criticalOnTop(message); + } +} + +TaskDialogButton::TaskDialogButton(QString t, QString d, QMessageBox::StandardButton b) + : text(std::move(t)), description(std::move(d)), button(b) +{} + +TaskDialogButton::TaskDialogButton(QString t, QMessageBox::StandardButton b) + : TaskDialogButton(std::move(t), {}, b) +{} + +TaskDialog::TaskDialog(QWidget* parent, QString title) + : m_dialog(new QDialog(parent)), ui(new Ui::TaskDialog), m_title(std::move(title)), + m_icon(QMessageBox::NoIcon), m_result(QMessageBox::Cancel), m_width(-1), + m_rememberCheck(nullptr), m_rememberCombo(nullptr) +{ + ui->setupUi(m_dialog.get()); +} + +TaskDialog::~TaskDialog() = default; + +TaskDialog& TaskDialog::title(const QString& s) +{ + m_title = s; + return *this; +} + +TaskDialog& TaskDialog::main(const QString& s) +{ + m_main = s; + return *this; +} + +TaskDialog& TaskDialog::content(const QString& s) +{ + m_content = s; + return *this; +} + +TaskDialog& TaskDialog::details(const QString& s) +{ + m_details = s; + return *this; +} + +TaskDialog& TaskDialog::icon(QMessageBox::Icon i) +{ + m_icon = i; + return *this; +} + +TaskDialog& TaskDialog::button(TaskDialogButton b) +{ + m_buttons.emplace_back(std::move(b)); + return *this; +} + +TaskDialog& TaskDialog::remember(const QString& action, const QString& file) +{ + m_rememberAction = action; + m_rememberFile = file; + return *this; +} + +void TaskDialog::addContent(QWidget* w) +{ + auto* ly = static_cast(ui->contentPanel->layout()); + + // add before spacer + ly->insertWidget(ly->count() - 1, w); +} + +void TaskDialog::setWidth(int w) +{ + m_width = w; +} + +QMessageBox::StandardButton TaskDialog::exec() +{ + const auto b = checkMemory(); + if (b != QMessageBox::NoButton) { + return b; + } + + setDialog(); + setWidgets(); + setChoices(); + setButtons(); + setDetails(); + + if (m_width >= 0) { + ui->topPanel->setMinimumWidth(m_width); + } else { + ui->topPanel->setMinimumWidth(400); + } + + m_dialog->adjustSize(); + + if (!m_dialog->parent()) { + // no parent, make sure it's shown on top + m_dialog->show(); + m_dialog->activateWindow(); + m_dialog->raise(); + } + + if (m_dialog->exec() != QDialog::Accepted) { + return QMessageBox::Cancel; + } + + rememberChoice(); + return m_result; +} + +QMessageBox::StandardButton TaskDialog::checkMemory() const +{ + if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) { + return QMessageBox::NoButton; + } + + const auto b = QuestionBoxMemory::getMemory(m_rememberAction, m_rememberFile); + + const auto logName = m_rememberAction + + (m_rememberFile.isEmpty() ? "" : QString("/") + m_rememberFile); + + if (b == QDialogButtonBox::NoButton) { + log::debug("{}: asking because the user has not set a choice before", logName); + } else { + log::debug("{}: not asking because user always wants response {}", logName, + QuestionBoxMemory::buttonToString(b)); + } + + return static_cast(b); +} + +void TaskDialog::rememberChoice() +{ + if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) { + // nothing + return; + } + + const auto b = static_cast(m_result); + + if (m_rememberCheck) { + // action only + if (m_rememberCheck->isChecked()) { + QuestionBoxMemory::setWindowMemory(m_rememberAction, b); + } + } else if (m_rememberCombo) { + Q_ASSERT(m_rememberCombo->count() == 3); + + if (m_rememberCombo->currentIndex() == 1) { + // remember action + QuestionBoxMemory::setWindowMemory(m_rememberAction, b); + } else if (m_rememberCombo->currentIndex() == 2) { + // remember file + QuestionBoxMemory::setFileMemory(m_rememberAction, m_rememberFile, b); + } + } +} + +void TaskDialog::setButtons() +{ + if (m_buttons.empty()) { + setStandardButtons(); + } else { + setCommandButtons(); + } + + m_expander.reset(new ExpanderWidget(ui->detailsExpander, ui->detailsWidget)); +} + +void TaskDialog::setStandardButtons() +{ + ui->standardButtonsPanel->show(); + ui->commandButtonsPanel->hide(); + + for (auto* b : ui->standardButtons->buttons()) { + ui->standardButtons->removeButton(b); + } + + ui->standardButtons->addButton(QDialogButtonBox::Ok); + + QObject::connect(ui->standardButtons, &QDialogButtonBox::clicked, [&](auto* b) { + m_result = static_cast( + ui->standardButtons->standardButton(b)); + + m_dialog->accept(); + }); +} + +void TaskDialog::setCommandButtons() +{ + ui->standardButtonsPanel->hide(); + ui->commandButtonsPanel->show(); + + deleteChildWidgets(ui->commandButtons); + + for (auto&& b : m_buttons) { + auto* cb = new QCommandLinkButton(b.text, b.description); + + QObject::connect(cb, &QAbstractButton::clicked, [&] { + m_result = b.button; + m_dialog->accept(); + }); + + ui->commandButtons->layout()->addWidget(cb); + } +} + +void TaskDialog::setDetails() +{ + if (m_details.isEmpty()) { + ui->detailsExpander->setEnabled(false); + return; + } + + ui->details->setPlainText(m_details); + + setVisibleLines(ui->details, 10); + setFontPercent(ui->details, 0.9); + + const QColor bg = detailsColor(); + + ui->detailsPanel->setStyleSheet(QString("background-color: rgb(%1, %2, %3)") + .arg(bg.redF() * 255) + .arg(bg.greenF() * 255) + .arg(bg.blueF() * 255)); +} + +void TaskDialog::setDialog() +{ +#ifdef _WIN32 + m_dialog->setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint); +#else + m_dialog->setWindowFlags(Qt::Dialog); +#endif + m_dialog->layout()->setSizeConstraint(QLayout::SetFixedSize); + + m_dialog->setWindowTitle(m_title); +} + +void TaskDialog::setWidgets() +{ + ui->main->setText(m_main); + setFontPercent(ui->main, 1.5); + + ui->content->setText(m_content); + ui->content->setVisible(!m_content.isEmpty()); + + auto icon = standardIcon(m_icon); + + if (icon.isNull()) { + ui->iconPanel->hide(); + } else { + ui->iconPanel->show(); + ui->icon->setPixmap(std::move(icon)); + } +} + +void TaskDialog::setChoices() +{ + if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) { + ui->rememberPanel->hide(); + return; + } + + ui->rememberPanel->show(); + deleteChildWidgets(ui->rememberPanel); + + const auto tooltip = QObject::tr( + "You can reset these choices by clicking \"Reset Dialog Choices\" in the " + "General tab of the Settings"); + + if (!m_rememberAction.isEmpty() && !m_rememberFile.isEmpty()) { + // both + m_rememberCombo = new QComboBox; + m_rememberCombo->setToolTip(tooltip); + + m_rememberCombo->addItem(QObject::tr("Always ask")); + m_rememberCombo->addItem(QObject::tr("Remember my choice")); + m_rememberCombo->addItem( + QObject::tr("Remember my choice for %1").arg(m_rememberFile)); + + ui->rememberPanel->layout()->setAlignment(Qt::AlignLeft); + ui->rememberPanel->layout()->addWidget(m_rememberCombo); + } else if (!m_rememberAction.isEmpty() || !m_rememberFile.isEmpty()) { + // either + m_rememberCheck = new QCheckBox(QObject::tr("Remember my choice")); + m_rememberCheck->setToolTip(tooltip); + ui->rememberPanel->layout()->addWidget(m_rememberCheck); + } +} + +QColor TaskDialog::detailsColor() const +{ + auto b = std::make_unique(); + m_dialog->style()->polish(b.get()); + return b->palette().color(b->backgroundRole()); +} + +QPixmap TaskDialog::standardIcon(QMessageBox::Icon icon) const +{ + QStyle* s = m_dialog->style(); + QIcon i; + + switch (icon) { + case QMessageBox::Information: + i = s->standardIcon(QStyle::SP_MessageBoxInformation, 0, m_dialog.get()); + break; + case QMessageBox::Warning: + i = s->standardIcon(QStyle::SP_MessageBoxWarning, 0, m_dialog.get()); + break; + case QMessageBox::Critical: + i = s->standardIcon(QStyle::SP_MessageBoxCritical, 0, m_dialog.get()); + break; + case QMessageBox::Question: + i = s->standardIcon(QStyle::SP_MessageBoxQuestion, 0, m_dialog.get()); + break; + case QMessageBox::NoIcon: + [[fallthrough]]; + default: + break; + } + + if (i.isNull()) { + return {}; + } + + QWindow* window = m_dialog->windowHandle(); + if (!window) { + if (const QWidget* nativeParent = m_dialog->nativeParentWidget()) + window = nativeParent->windowHandle(); + } + + const int iconSize = s->pixelMetric(QStyle::PM_MessageBoxIconSize, 0, m_dialog.get()); + + return i.pixmap(QSize(iconSize, iconSize)); +} + +void TaskDialog::setVisibleLines(QPlainTextEdit* w, int lines) +{ + QTextDocument* d = w->document(); + QFontMetrics fm(d->defaultFont()); + + const QMargins margins = ui->details->contentsMargins(); + + double height = 0; + + // lines + height += fm.lineSpacing() * lines; + + // top and bottom margins for document and frame + height += (d->documentMargin() + ui->details->frameWidth()) * 2; + + // widget margins + height += margins.top() + margins.bottom(); + + w->setMaximumHeight(static_cast(std::round(height))); +} + +void TaskDialog::setFontPercent(QWidget* w, double p) +{ + auto f = w->font(); + + if (f.pointSizeF() > 0) { + f.setPointSizeF(f.pointSizeF() * p); + } else if (f.pixelSize() > 0) { + f.setPixelSize(static_cast(std::round(f.pixelSize() * p))); + } + + w->setFont(f); +} + +} // namespace MOBase diff --git a/libs/uibase/src/safewritefile.cpp b/libs/uibase/src/safewritefile.cpp new file mode 100644 index 0000000..fb0819f --- /dev/null +++ b/libs/uibase/src/safewritefile.cpp @@ -0,0 +1,84 @@ +/* +Copyright (C) 2014 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include +#include +#include +#include + +namespace MOBase +{ + +#ifdef _WIN32 + +SafeWriteFile::SafeWriteFile(const QString& fileName) : m_SaveFile(fileName) +{ + if (!m_SaveFile.open(QIODeviceBase::WriteOnly)) { + const auto av = + static_cast(QStorageInfo(m_SaveFile.fileName()).bytesAvailable()); + + log::error( + "failed to create temporary file for '{}', error {} ('{}'), {:.3f}GB available", + m_SaveFile.fileName(), m_SaveFile.error(), m_SaveFile.errorString(), + (av / 1024 / 1024 / 1024)); + + throw Exception( + QObject::tr( + "Failed to save '%1', could not create a temporary file: %2 (error %3)") + .arg(m_SaveFile.fileName()) + .arg(m_SaveFile.errorString()) + .arg(m_SaveFile.error())); + } +} + +QSaveFile* SafeWriteFile::operator->() +{ + Q_ASSERT(m_SaveFile.isOpen()); + return &m_SaveFile; +} + +#else // Linux — use plain QFile (QSaveFile is unreliable across filesystems) + +SafeWriteFile::SafeWriteFile(const QString& fileName) : m_File(fileName) +{ + if (!m_File.open(QIODeviceBase::WriteOnly | QIODeviceBase::Truncate)) { + const auto av = + static_cast(QStorageInfo(m_File.fileName()).bytesAvailable()); + + log::error("failed to open '{}' for writing, error {} ('{}'), {:.3f}GB available", + m_File.fileName(), m_File.error(), m_File.errorString(), + (av / 1024 / 1024 / 1024)); + + throw Exception( + QObject::tr("Failed to save '%1': %2 (error %3)") + .arg(m_File.fileName()) + .arg(m_File.errorString()) + .arg(m_File.error())); + } +} + +DirectWriteFile* SafeWriteFile::operator->() +{ + Q_ASSERT(m_File.isOpen()); + return &m_File; +} + +#endif + +} // namespace MOBase diff --git a/libs/uibase/src/scopeguard.cpp b/libs/uibase/src/scopeguard.cpp new file mode 100644 index 0000000..a671d1e --- /dev/null +++ b/libs/uibase/src/scopeguard.cpp @@ -0,0 +1,6 @@ +#include + +namespace MOBase +{ + +} // namespace MOBase diff --git a/libs/uibase/src/sortabletreewidget.cpp b/libs/uibase/src/sortabletreewidget.cpp new file mode 100644 index 0000000..5cfbdd4 --- /dev/null +++ b/libs/uibase/src/sortabletreewidget.cpp @@ -0,0 +1,116 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2013 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 +#include + +namespace MOBase +{ + +SortableTreeWidget::SortableTreeWidget(QWidget* parent) + : QTreeWidget(parent), m_LocalMoveOnly(false) +{} + +void SortableTreeWidget::setLocalMoveOnly(bool localOnly) +{ + m_LocalMoveOnly = localOnly; +} + +void SortableTreeWidget::dropEvent(QDropEvent* event) +{ + // only react on internal drag&drop + if (event->source() == this) { + QTreeWidget::dropEvent(event); + } +} + +Qt::DropActions SortableTreeWidget::supportedDropActions() const +{ + // we do our own drop handling, this ensures dropMimeData is called. + return Qt::CopyAction; +} + +bool SortableTreeWidget::dropMimeData(QTreeWidgetItem* parent, int index, + const QMimeData*, Qt::DropAction) +{ + // TODO: ignores type of action set up in designer, always moves + return this->moveSelection(parent, index); +} + +bool SortableTreeWidget::moveSelection(QTreeWidgetItem* parent, int idx) +{ + QModelIndex parentIndex = indexFromItem(parent); + std::vector persistentIndices; + Q_FOREACH (const QModelIndex& index, selectedIndexes()) { + if (index == parentIndex) + return false; + if (m_LocalMoveOnly && (parentIndex != index.parent())) + return false; + if (index.column() != 0) + continue; + persistentIndices.push_back(index); + } + + int targetRow = -1; + if (itemsExpandable() || !parentIndex.isValid()) { + targetRow = model()->index(idx, 0, parentIndex).row(); + + if (targetRow < 0) { + targetRow = idx; + } + } else { + // if items aren't expandable, we can't be placing items on sublevels + targetRow = parentIndex.row(); + parentIndex = QModelIndex(); + } + + // remove items from the list, store in temporary location + QList temp; + for (auto iter = persistentIndices.rbegin(); iter != persistentIndices.rend(); + ++iter) { + QTreeWidgetItem* item = itemFromIndex(*iter); + if (item == nullptr) + continue; + if ((item == nullptr) || (item->parent() == nullptr)) { + temp.append(takeTopLevelItem(iter->row())); + } else { + temp.append(item->parent()->takeChild(iter->row())); + } + } + + if (idx == -1) { + // append + if (parentIndex.isValid()) { + parent->addChildren(temp); + } else { + addTopLevelItems(temp); + } + } else { + if (parentIndex.isValid()) { + parent->insertChildren(std::min(targetRow, parent->childCount()), temp); + } else { + insertTopLevelItems(std::min(targetRow, topLevelItemCount()), temp); + } + } + emit itemsMoved(); + return true; +} + +} // namespace MOBase diff --git a/libs/uibase/src/steamutility.cpp b/libs/uibase/src/steamutility.cpp new file mode 100644 index 0000000..a9dd80a --- /dev/null +++ b/libs/uibase/src/steamutility.cpp @@ -0,0 +1,108 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2019 MO2 Contributors. 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 + +#include +#include +#include +#include +#include +#include + +namespace MOBase +{ + +// Lines that contains libraries are in the format: +// "1" "Path\to\library" +static const QRegularExpression + kSteamLibraryFilter("^\\s*\"(?[0-9]+)\"\\s*\"(?.*)\""); + +QString findSteam() +{ +#ifdef _WIN32 + QSettings steamRegistry("Valve", "Steam"); + return steamRegistry.value("SteamPath").toString(); +#else + // On Linux, check common Steam locations + QString homePath = QDir::homePath(); + QStringList candidates = { + homePath + "/.steam/steam", + homePath + "/.local/share/Steam", + homePath + "/.steam/debian-installation" + }; + for (const auto& path : candidates) { + if (QDir(path).exists()) { + return path; + } + } + return ""; +#endif +} + +QString findSteamGame(const QString& appName, const QString& validFile) +{ + QStringList libraryFolders; // list of Steam libraries to search + QDir steamDir(findSteam()); // Steam installation directory + + // Can do nothing if Steam doesn't exist + if (!steamDir.exists()) + return ""; + + // The Steam install is always a valid library + libraryFolders << steamDir.absolutePath(); + + // Search libraryfolders.vdf for additional libraries + QFile libraryFoldersFile(steamDir.absoluteFilePath("steamapps/libraryfolders.vdf")); + if (libraryFoldersFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QTextStream in(&libraryFoldersFile); + while (!in.atEnd()) { + QString line = in.readLine(); + QRegularExpressionMatch match = kSteamLibraryFilter.match(line); + if (match.hasMatch()) { + QString folder = match.captured("path"); +#ifdef _WIN32 + folder.replace("/", "\\").replace("\\\\", "\\"); +#else + folder.replace("\\\\", "/").replace("\\", "/"); +#endif + libraryFolders << folder; + } + } + } + + // Search the Steam libraries for the game directory + for (auto library : libraryFolders) { + QDir libraryDir(library); + if (!libraryDir.cd("steamapps/common/" + appName)) + continue; + QString normalizedValidFile = validFile; + normalizedValidFile.replace("\\", "/"); + if (normalizedValidFile.startsWith('/')) { + normalizedValidFile.remove(0, 1); + } + if (normalizedValidFile.isEmpty() || libraryDir.exists(normalizedValidFile)) + return libraryDir.absolutePath(); + } + + return ""; +} + +} // namespace MOBase diff --git a/libs/uibase/src/strings.cpp b/libs/uibase/src/strings.cpp new file mode 100644 index 0000000..11087f4 --- /dev/null +++ b/libs/uibase/src/strings.cpp @@ -0,0 +1,46 @@ +#include + +#include +#include + +namespace MOBase +{ + +// this is strongly inspired from boost +class is_iequal +{ + std::locale m_loc; + +public: + is_iequal(const std::locale& loc = std::locale()) : m_loc{loc} {} + + template + bool operator()(const T1& Arg1, const T2& Arg2) const + { + return std::toupper(Arg1, m_loc) == std::toupper(Arg2, m_loc); + } +}; + +bool iequals(std::string_view lhs, std::string_view rhs) +{ + return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), is_iequal()); +} + +void ireplace_all(std::string& input, std::string_view search, + std::string_view replace) noexcept +{ + const auto search_length = static_cast(search.size()); + const auto replace_length = replace.size(); + + std::size_t i = 0; + while (input.size() - i >= search_length) { + if (iequals(std::string_view(input).substr(i, search_length), search)) { + input.replace(i, search_length, replace); + i += replace_length; + } else { + ++i; + } + } +} + +} // namespace MOBase diff --git a/libs/uibase/src/taskdialog.ui b/libs/uibase/src/taskdialog.ui new file mode 100644 index 0000000..727312d --- /dev/null +++ b/libs/uibase/src/taskdialog.ui @@ -0,0 +1,359 @@ + + + TaskDialog + + + + 0 + 0 + 436 + 316 + + + + + 0 + 0 + + + + Dialog + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 9 + + + 0 + + + 9 + + + 0 + + + + + icon + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + dummy main text + + + true + + + + + + + + 0 + 0 + + + + + 0 + 10 + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + dummy content text + + + true + + + + + + + + 0 + 0 + + + + + 0 + 10 + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + dummy button + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + dummy checkbox + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + QDialogButtonBox::Close + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + border: none; +text-align: left; + + + Details + + + + + + + + 0 + + + 0 + + + 0 + + + 9 + + + + + true + + + + + + + + + + + + + + + + + diff --git a/libs/uibase/src/taskprogressmanager.cpp b/libs/uibase/src/taskprogressmanager.cpp new file mode 100644 index 0000000..3bcffb0 --- /dev/null +++ b/libs/uibase/src/taskprogressmanager.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include + +namespace MOBase +{ + +TaskProgressManager& TaskProgressManager::instance() +{ + static TaskProgressManager s_Instance; + return s_Instance; +} + +void TaskProgressManager::forgetMe(quint32 id) +{ + if (m_Taskbar == nullptr) { + return; + } + auto iter = m_Percentages.find(id); + if (iter != m_Percentages.end()) { + m_Percentages.erase(iter); + } + showProgress(); +} + +void TaskProgressManager::updateProgress(quint32 id, qint64 value, qint64 max) +{ + QMutexLocker lock(&m_Mutex); + if (m_Taskbar == nullptr) { + return; + } + + if (value == max) { + auto iter = m_Percentages.find(id); + if (iter != m_Percentages.end()) { + m_Percentages.erase(iter); + } + } else { + m_Percentages[id] = std::make_pair(QTime::currentTime(), (value * 100) / max); + } + + showProgress(); +} + +quint32 TaskProgressManager::getId() +{ + QMutexLocker lock(&m_Mutex); + return m_NextId++; +} + +void TaskProgressManager::showProgress() +{ +#ifdef _WIN32 + if (!m_Percentages.empty()) { + m_Taskbar->SetProgressState(m_WinId, TBPF_NORMAL); + + QTime now = QTime::currentTime(); + unsigned long long total = 0; + unsigned long long count = 0; + + for (auto iter = m_Percentages.begin(); iter != m_Percentages.end();) { + if (iter->second.first.secsTo(now) < 15) { + total += static_cast(iter->second.second); + ++iter; + ++count; + } else { + log::debug("no progress in 15 seconds ({})", iter->second.first.secsTo(now)); + iter = m_Percentages.erase(iter); + } + } + + m_Taskbar->SetProgressValue(m_WinId, total, count * 100); + } else { + m_Taskbar->SetProgressState(m_WinId, TBPF_NOPROGRESS); + } +#else + // On Linux, taskbar progress is not supported in this implementation. + // Could integrate with D-Bus com.canonical.Unity.LauncherEntry or similar. + (void)m_Percentages; +#endif +} + +bool TaskProgressManager::tryCreateTaskbar() +{ +#ifdef _WIN32 + // try to find our main window + for (QWidget* widget : QApplication::topLevelWidgets()) { + QMainWindow* mainWin = qobject_cast(widget); + if (mainWin != nullptr) { + m_WinId = reinterpret_cast(mainWin->winId()); + } + } + + HRESULT result = 0; + if (m_WinId != nullptr) { + result = CoCreateInstance(CLSID_TaskbarList, 0, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&m_Taskbar)); + if (result == S_OK) { + return true; + } + } + + m_Taskbar = nullptr; + + if (m_CreateTries-- > 0) { + QTimer::singleShot(1000, this, SLOT(tryCreateTaskbar())); + } else { + log::warn("failed to create taskbar connection"); + } + return false; +#else + // On Linux, no Windows taskbar API + m_Taskbar = nullptr; + return false; +#endif +} + +TaskProgressManager::TaskProgressManager() + : m_NextId(1), m_CreateTries(10), m_WinId(nullptr), m_Taskbar(nullptr) +{} + +} // namespace MOBase diff --git a/libs/uibase/src/textviewer.cpp b/libs/uibase/src/textviewer.cpp new file mode 100644 index 0000000..8c7ac0f --- /dev/null +++ b/libs/uibase/src/textviewer.cpp @@ -0,0 +1,278 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 +#include +#include +#include +#include "ui_textviewer.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MOBase +{ + +TextViewer::TextViewer(const QString& title, QWidget* parent) + : QDialog(parent), ui(new Ui::TextViewer), m_FindDialog(nullptr) +{ + ui->setupUi(this); + setWindowTitle(title); + m_EditorTabs = findChild("editorTabs"); + connect(ui->showWhitespace, SIGNAL(stateChanged(int)), this, + SLOT(showWhitespaceChanged(int))); +} + +TextViewer::~TextViewer() +{ + delete ui; +} + +void TextViewer::closeEvent(QCloseEvent* event) +{ + if (!m_Modified.empty()) { + for (std::set::iterator iter = m_Modified.begin(); + iter != m_Modified.end(); ++iter) { + QMessageBox::StandardButton res = QMessageBox::question( + this, tr("Save changes?"), + tr("Do you want to save changes to %1?").arg((*iter)->documentTitle()), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (res == QMessageBox::Yes) { + saveFile(*iter); + } else if (res == QMessageBox::Cancel) { + event->ignore(); + break; + } + } + } +} + +void TextViewer::find() +{ + if (!m_FindDialog) { + m_FindDialog = new FindDialog(this); + connect(m_FindDialog, SIGNAL(findNext()), this, SLOT(findNext())); + connect(m_FindDialog, SIGNAL(patternChanged(QString)), this, + SLOT(patternChanged(QString))); + } + + m_FindDialog->show(); + m_FindDialog->raise(); + m_FindDialog->activateWindow(); +} + +void TextViewer::patternChanged(QString newPattern) +{ + m_FindPattern = newPattern; +} + +void TextViewer::findNext() +{ + if (m_FindPattern.length() == 0) { + return; + } + + QWidget* currentPage = m_EditorTabs->currentWidget(); + QTextEdit* editor = currentPage->findChild("editorView"); + + if (editor->find(m_FindPattern)) { + // found text + return; + } else { + // reached the bottom and no text found, + // we wrap around once. + + // save current cursor + auto oldCursor = editor->textCursor(); + + editor->moveCursor(QTextCursor::Start); + + // search again from the top + if (editor->find(m_FindPattern)) { + // found something, keep new cursor position. + return; + } else { + // there are no matches in the document, + // restore previous cursor. + editor->setTextCursor(oldCursor); + } + } +} + +void TextViewer::showWhitespaceChanged(int state) +{ + for (int i = 0; i < m_EditorTabs->count(); ++i) { + QTextEdit* editor = m_EditorTabs->widget(i)->findChild(); + if (editor != nullptr) { + auto document = editor->document(); + auto textOption = document->defaultTextOption(); + auto flags = textOption.flags(); + if (state == Qt::Unchecked) + flags = flags & (~QTextOption::ShowTabsAndSpaces); + else + flags = flags | QTextOption::ShowTabsAndSpaces; + textOption.setFlags(flags); + document->setDefaultTextOption(textOption); + editor->setDocument(document); + } + } +} + +bool TextViewer::eventFilter(QObject* object, QEvent* event) +{ + if (event->type() == QEvent::ShortcutOverride) { + QKeyEvent* keyEvent = static_cast(event); + if (keyEvent->matches(QKeySequence::Find)) { + find(); + } else if (keyEvent->matches(QKeySequence::FindNext)) { + findNext(); + } + } + return QDialog::eventFilter(object, event); +} + +void TextViewer::setDescription(const QString& description) +{ + QLabel* descriptionLabel = findChild("descriptionLabel"); + descriptionLabel->setText(description); +} + +void TextViewer::saveFile(const QTextEdit* editor) +{ + bool write = true; + QMessageBox::StandardButton buttonPressed = QMessageBox::Ignore; + QFile file(editor->documentTitle()); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + write = false; + QFileInfo fileInfo(file.fileName()); + buttonPressed = + MOBase::TaskDialog(qApp->activeModalWidget(), + QObject::tr("INI file is read-only")) + .main(QObject::tr("INI file is read-only")) + .content(QObject::tr("Mod Organizer is attempting to write to \"%1\" which " + "is currently set to read-only.") + .arg(fileInfo.fileName())) + .icon(QMessageBox::Warning) + .button({QObject::tr("Clear the read-only flag"), QMessageBox::Yes}) + .button({QObject::tr("Allow the write once"), + QObject::tr("The file will be set to read-only again."), + QMessageBox::Ignore}) + .button({QObject::tr("Skip this file"), QMessageBox::No}) + .remember("clearReadOnly", fileInfo.fileName()) + .exec(); + + if (buttonPressed & (QMessageBox::Yes | QMessageBox::Ignore)) { + file.setPermissions(file.permissions() | QFile::WriteUser); + } + + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + reportError(tr("failed to write to %1").arg(editor->documentTitle())); + } else { + write = true; + } + } + + if (write) { + file.write(editor->toPlainText().toUtf8().replace('\n', "\r\n")); + file.close(); + } + + if (buttonPressed == QMessageBox::Ignore) { + file.setPermissions(file.permissions() & ~(QFile::WriteUser)); + } +} + +void TextViewer::saveFile() +{ + QWidget* currentPage = m_EditorTabs->currentWidget(); + QTextEdit* editor = currentPage->findChild("editorView"); + saveFile(editor); + + m_Modified.erase(editor); +} + +void TextViewer::modified() +{ + QWidget* currentPage = m_EditorTabs->currentWidget(); + QTextEdit* editor = currentPage->findChild("editorView"); + + m_Modified.insert(editor); +} + +void TextViewer::addFile(const QString& fileName, bool writable) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + throw Exception(tr("file not found: %1").arg(fileName)); + } + QByteArray temp = file.readAll(); + + QWidget* page = new QWidget(); + QVBoxLayout* layout = new QVBoxLayout(page); + QTextEdit* editor = new QTextEdit(page); + QTextDocument* document = new QTextDocument(page); + if (ui->showWhitespace->isChecked()) { + QTextOption option; + option.setFlags(QTextOption::ShowTabsAndSpaces); + document->setDefaultTextOption(option); + } + editor->setDocument(document); + editor->setAcceptRichText(false); + editor->setPlainText(QString(temp)); + editor->setLineWrapMode(QTextEdit::NoWrap); + editor->setObjectName("editorView"); + editor->setDocumentTitle(fileName); + editor->installEventFilter(this); + editor->setReadOnly(!writable); + + // set text highlighting color in inactive window equal to text hightlighting color in + // active window + QPalette palette = editor->palette(); + palette.setColor(QPalette::Inactive, QPalette::Highlight, + palette.color(QPalette::Active, QPalette::Highlight)); + palette.setColor(QPalette::Inactive, QPalette::HighlightedText, + palette.color(QPalette::Active, QPalette::HighlightedText)); + editor->setPalette(palette); + + // add hotkeys for searching through the document + QAction* findAction = new QAction(QString("&Find"), editor); + findAction->setShortcut(QKeySequence::Find); + editor->addAction(findAction); + QAction* findNextAction = new QAction(QString("Find &Next"), editor); + findAction->setShortcut(QKeySequence::FindNext); + editor->addAction(findNextAction); + + layout->addWidget(editor); + if (writable) { + QPushButton* saveBtn = new QPushButton(tr("Save"), page); + layout->addWidget(saveBtn); + connect(saveBtn, SIGNAL(clicked()), this, SLOT(saveFile())); + connect(editor, SIGNAL(textChanged()), this, SLOT(modified())); + } + page->setLayout(layout); + m_EditorTabs->addTab(page, QFileInfo(fileName).fileName()); +} +} // namespace MOBase diff --git a/libs/uibase/src/textviewer.ui b/libs/uibase/src/textviewer.ui new file mode 100644 index 0000000..e2af30e --- /dev/null +++ b/libs/uibase/src/textviewer.ui @@ -0,0 +1,55 @@ + + + TextViewer + + + + 0 + 0 + 607 + 528 + + + + Log Viewer + + + + + + + + Placeholder + + + + + + + Show Whitespace + + + true + + + + + + + + + + 0 + 0 + + + + -1 + + + + + + + + diff --git a/libs/uibase/src/tutorabledialog.cpp b/libs/uibase/src/tutorabledialog.cpp new file mode 100644 index 0000000..61aad62 --- /dev/null +++ b/libs/uibase/src/tutorabledialog.cpp @@ -0,0 +1,40 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with Mod Organizer. If not, see . +*/ + +#include + +namespace MOBase +{ + +TutorableDialog::TutorableDialog(const QString& name, QWidget* parent) + : QDialog(parent), m_Tutorial(this, name) +{} + +void TutorableDialog::showEvent(QShowEvent* event) +{ + m_Tutorial.registerControl(); + QDialog::showEvent(event); +} + +void TutorableDialog::resizeEvent(QResizeEvent* event) +{ + m_Tutorial.resize(event->size()); + QDialog::resizeEvent(event); +} +} // namespace MOBase diff --git a/libs/uibase/src/tutorialcontrol.cpp b/libs/uibase/src/tutorialcontrol.cpp new file mode 100644 index 0000000..fee19f4 --- /dev/null +++ b/libs/uibase/src/tutorialcontrol.cpp @@ -0,0 +1,384 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with Mod Organizer. If not, see . +*/ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace MOBase +{ + +TutorialControl::TutorialControl(const TutorialControl& reference) + : QObject(reference.parent()), m_TargetControl(reference.m_TargetControl), + m_Name(reference.m_Name), m_TutorialView(nullptr), + m_Manager(TutorialManager::instance()), m_ExpectedTab(0), + m_CurrentClickControl(nullptr) +{} + +TutorialControl::TutorialControl(QWidget* targetControl, const QString& name) + : QObject(nullptr), m_TargetControl(targetControl), m_Name(name), + m_TutorialView(nullptr), m_Manager(TutorialManager::instance()), m_ExpectedTab(0), + m_CurrentClickControl(nullptr) +{} + +TutorialControl::~TutorialControl() +{ + m_Manager.unregisterControl(m_Name); + finish(); +} + +void TutorialControl::registerControl() +{ + m_Manager.registerControl(m_Name, this); +} + +void TutorialControl::resize(const QSize& size) +{ + if (m_TutorialView != nullptr) { + m_TutorialView->resize(size.width(), size.height()); + } +} + +void TutorialControl::expose(const QString& widgetName, QObject* widget) +{ + m_ExposedObjects.push_back(std::make_pair(widgetName, widget)); +} + +static QString canonicalPath(const QString& path) +{ +#ifdef _WIN32 + std::unique_ptr buffer(new wchar_t[32768]); + DWORD res = ::GetShortPathNameW((wchar_t*)path.utf16(), buffer.get(), 32768); + if (res == 0) { + return path; + } + res = ::GetLongPathNameW(buffer.get(), buffer.get(), 32768); + if (res == 0) { + return path; + } + return QString::fromWCharArray(buffer.get()); +#else + QFileInfo fi(path); + QString canonical = fi.canonicalFilePath(); + return canonical.isEmpty() ? path : canonical; +#endif +} + +void TutorialControl::startTutorial(const QString& tutorial) +{ + if (m_TutorialView == nullptr) { + m_TutorialView = new QQuickWidget(m_TargetControl); + m_TutorialView->setResizeMode(QQuickWidget::SizeRootObjectToView); + m_TutorialView->setAttribute(Qt::WA_TranslucentBackground); + m_TutorialView->setAttribute(Qt::WA_AlwaysStackOnTop); + m_TutorialView->setClearColor(Qt::transparent); + m_TutorialView->setStyleSheet("background: transparent"); + m_TutorialView->setObjectName("tutorialView"); + m_TutorialView->rootContext()->setContextProperty("manager", &m_Manager); + + QString qmlName = + canonicalPath(QCoreApplication::applicationDirPath() + "/tutorials") + + "/tutorials_" + m_Name.toLower() + ".qml"; + QUrl qmlSource = QUrl::fromLocalFile(qmlName); + + m_TutorialView->setSource(qmlSource); + m_TutorialView->resize(m_TargetControl->width(), m_TargetControl->height()); + m_TutorialView->rootContext()->setContextProperty("scriptName", tutorial); + m_TutorialView->rootContext()->setContextProperty("tutorialControl", this); + m_TutorialView->rootContext()->setContextProperty("applicationWindow", + m_TargetControl); + m_TutorialView->rootContext()->setContextProperty("organizer", + m_Manager.organizerCore()); + + for (std::vector>::const_iterator iter = + m_ExposedObjects.begin(); + iter != m_ExposedObjects.end(); ++iter) { + m_TutorialView->rootContext()->setContextProperty(iter->first, iter->second); + } + m_TutorialView->show(); + m_TutorialView->raise(); + if (!QMetaObject::invokeMethod(m_TutorialView->rootObject(), "init")) { + reportError(tr( + "Tutorial failed to start, please check \"mo_interface.log\" for details.")); + m_TutorialView->close(); + } + } +} + +void TutorialControl::lockUI(bool locked) +{ + m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, !locked); + + QMetaObject::invokeMethod(m_TutorialView->rootObject(), "enableBackground", + Q_ARG(QVariant, QVariant(locked))); +} + +void TutorialControl::simulateClick(int x, int y) +{ + bool wasTransparent = m_TutorialView->testAttribute(Qt::WA_TransparentForMouseEvents); + if (!wasTransparent) { + m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, true); + } + QWidget* hitControl = m_TargetControl->childAt(x, y); + QPoint globalPos = m_TargetControl->mapToGlobal(QPoint(x, y)); + QPoint hitPos = hitControl->mapFromGlobal(globalPos); + QMouseEvent* downEvent = new QMouseEvent( + QEvent::MouseButtonPress, QPointF(hitPos), QPointF(globalPos), + Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + QMouseEvent* upEvent = + new QMouseEvent(QEvent::MouseButtonRelease, QPointF(hitPos), QPointF(globalPos), + Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + + qApp->postEvent(hitControl, (QEvent*)downEvent); + qApp->postEvent(hitControl, (QEvent*)upEvent); + + if (!wasTransparent) { + m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, false); + } +} + +QWidget* TutorialControl::getChild(const QString& name) +{ + if (m_TargetControl != nullptr) { + return m_TargetControl->findChild(name); + } else { + return nullptr; + } +} + +void TutorialControl::finish() +{ + if (m_TutorialView != nullptr) { + m_TutorialView->deleteLater(); + } + m_TutorialView = nullptr; + m_TutorialView = nullptr; +} + +QRect TutorialControl::getRect(const QString& widgetName) +{ + if (m_TargetControl != nullptr) { + QWidget* widget = m_TargetControl->findChild(widgetName); + if (widget != nullptr) { + QRect res = widget->rect(); + QPoint pos = widget->mapTo(m_TargetControl, res.topLeft()); + res.moveTopLeft(pos); + return res; + } else { + log::error("{} not found", widgetName); + return QRect(); + } + } else { + return QRect(); + } +} + +QRect TutorialControl::getActionRect(const QString& widgetName) +{ + if (m_TargetControl != nullptr) { + QToolBar* toolBar = m_TargetControl->findChild("toolBar"); + foreach (QAction* action, toolBar->actions()) { + if (action->objectName() == widgetName) { + return toolBar->actionGeometry(action); + } + } + } + return QRect(); +} + +QRect TutorialControl::getMenuRect(const QString&) +{ + if (m_TargetControl != nullptr) { + QMenuBar* menuBar = m_TargetControl->findChild("menuBar"); + return menuBar->geometry(); + } + return QRect(); +} + +void TutorialControl::nextTutorialStepProxy() +{ + + if (m_TutorialView != nullptr) { + QObject* background = m_TutorialView->rootObject(); + + QTimer::singleShot(1, background, SLOT(nextStep())); + lockUI(true); + + bool success = true; + for (QMetaObject::Connection connection : m_Connections) { + if (!disconnect(connection)) { + success = false; + } + } + m_Connections.clear(); + if (!success) { + log::error("failed to disconnect tutorial proxy"); + } + } else { + log::error("failed to proceed to next tutorial step"); + finish(); + } +} + +void TutorialControl::tabChangedProxy(int selected) +{ + if ((m_TutorialView != nullptr) && (selected == m_ExpectedTab)) { + QObject* background = m_TutorialView->rootObject(); + QTimer::singleShot(1, background, SLOT(nextStep())); + lockUI(true); + bool success = true; + for (QMetaObject::Connection connection : m_Connections) { + if (!disconnect(connection)) { + success = false; + } + } + m_Connections.clear(); + if (!success) { + log::error("failed to disconnect tab-changed proxy"); + } + } +} + +bool TutorialControl::waitForAction(const QString& actionName) +{ + if (m_TargetControl != nullptr) { + QAction* action = m_TargetControl->findChild(actionName); + if (action == nullptr) { + log::error("no action \"{}\" in control \"{}\"", actionName, m_Name); + return false; + } + if (action->isEnabled()) { + if (action->menu() != nullptr) { + m_Connections.append(connect(action->menu(), SIGNAL(aboutToShow()), this, + SLOT(nextTutorialStepProxy()))); + } else { + m_Connections.append( + connect(action, SIGNAL(triggered()), this, SLOT(nextTutorialStepProxy()))); + } + lockUI(false); + return true; + } else { + return false; + } + } else { + return false; + } +} + +bool TutorialControl::waitForButton(const QString& buttonName) +{ + if (m_TargetControl != nullptr) { + QAbstractButton* button = m_TargetControl->findChild(buttonName); + if (button == nullptr) { + log::error("no button \"{}\" in control \"{}\"", buttonName, m_Name); + return false; + } + if (button->isEnabled()) { + m_Connections.append( + connect(button, SIGNAL(pressed()), this, SLOT(nextTutorialStepProxy()))); + lockUI(false); + return true; + } else { + return false; + } + } else { + return false; + } +} + +bool TutorialControl::waitForTabOpen(const QString& tabControlName, const QString& tab) +{ + if (m_TargetControl != nullptr) { + QTabWidget* tabWidget = m_TargetControl->findChild(tabControlName); + if (tabWidget == nullptr) { + log::error("no tab widget \"{}\" in control \"{}\"", tabControlName, m_Name); + return false; + } + if (tabWidget->findChild(tab) == nullptr) { + log::error("no widget \"{}\" found in tab widget \"{}\"", tab, tabControlName); + return false; + } + int tabIndex = tabWidget->indexOf(tabWidget->findChild(tab)); + if (tabIndex == -1) { + log::error("widget \"{}\" does not appear to be a tab in tab widget \"{}\"", tab, + tabControlName); + return false; + } + if (tabWidget->isEnabled()) { + if (tabWidget->currentIndex() != tabIndex) { + m_ExpectedTab = tabIndex; + m_Connections.append(connect(tabWidget, SIGNAL(currentChanged(int)), this, + SLOT(tabChangedProxy(int)))); + lockUI(false); + } else { + QObject* background = m_TutorialView->rootObject(); + QTimer::singleShot(1, background, SLOT(nextStep())); + lockUI(true); + } + return true; + } else { + return false; + } + } else { + return false; + } +} + +const QString TutorialControl::getTabName(const QString& tabControlName) +{ + if (m_TargetControl != nullptr) { + QTabWidget* tabWidget = m_TargetControl->findChild(tabControlName); + if (tabWidget == nullptr) { + log::error("no tab widget \"{}\" in control \"{}\"", tabControlName, m_Name); + return QString(); + } + if (tabWidget->currentIndex() == -1) { + return QString(); + } else { + return tabWidget->currentWidget()->objectName(); + } + } else { + return QString(); + } +} +} // namespace MOBase diff --git a/libs/uibase/src/tutorialmanager.cpp b/libs/uibase/src/tutorialmanager.cpp new file mode 100644 index 0000000..58191e7 --- /dev/null +++ b/libs/uibase/src/tutorialmanager.cpp @@ -0,0 +1,112 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with Mod Organizer. If not, see . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MOBase +{ + +TutorialManager* TutorialManager::s_Instance = nullptr; + +TutorialManager::TutorialManager(const QString& tutorialPath, QObject* organizerCore) + : m_TutorialPath(tutorialPath), m_OrganizerCore(organizerCore) +{} + +void TutorialManager::init(const QString& tutorialPath, QObject* organizerCore) +{ + if (s_Instance != nullptr) { + delete s_Instance; + } + s_Instance = new TutorialManager(tutorialPath, organizerCore); +} + +TutorialManager& TutorialManager::instance() +{ + if (s_Instance == nullptr) { + throw Exception(tr("tutorial manager not set up yet")); + } + return *s_Instance; +} + +void TutorialManager::activateTutorial(const QString& windowName, + const QString& tutorialName) +{ + std::map::iterator iter = m_Controls.find(windowName); + if (iter != m_Controls.end()) { + // control already visible, start tutorial right away + iter->second->startTutorial(m_TutorialPath + tutorialName); + } else { + m_PendingTutorials[windowName] = tutorialName; + } +} + +void TutorialManager::finishWindowTutorial(const QString& windowName) +{ + emit windowTutorialFinished(windowName); + // QSettings &settings = Settings::instance().directInterface(); + // settings.setValue(QString("CompletedWindowTutorials/") + windowName, true); +} + +bool TutorialManager::hasTutorial(const QString& tutorialName) +{ + return QFile::exists(m_TutorialPath + tutorialName); +} + +QWidget* TutorialManager::findControl(const QString& controlName) +{ + QWidget* mainWindow = qApp->activeWindow(); + if (mainWindow != nullptr) { + return mainWindow->findChild(controlName); + } else { + return nullptr; + } +} + +void TutorialManager::registerControl(const QString& windowName, + TutorialControl* control) +{ + m_Controls[windowName] = control; + std::map::iterator iter = m_PendingTutorials.find(windowName); + if (iter != m_PendingTutorials.end()) { + // there is a pending tutorial for this window, display it + QTimer::singleShot(0, [control, tutorial = m_TutorialPath + iter->second] { + control->startTutorial(tutorial); + }); + m_PendingTutorials.erase(iter); + } +} + +void TutorialManager::unregisterControl(const QString& windowName) +{ + std::map::iterator iter = m_Controls.find(windowName); + if (iter != m_Controls.end()) { + m_Controls.erase(iter); + } else { + log::warn("failed to remove tutorial control {}", windowName); + } +} +} // namespace MOBase diff --git a/libs/uibase/src/uibase_en.ts b/libs/uibase/src/uibase_en.ts new file mode 100644 index 0000000..defc545 --- /dev/null +++ b/libs/uibase/src/uibase_en.ts @@ -0,0 +1,407 @@ + + + + + FilterWidget + + + Filter options + + + + + Use regular expressions + + + + + Use regular expressions in filters + + + + + Case sensitive + + + + + Make regular expressions case sensitive (/i) + leave "(/i)" verbatim + + + + + Extended + + + + + Ignores unescaped whitespace in regular expressions (/x) + leave "(/x)" verbatim + + + + + Keep selection in view + + + + + Scroll to keep the current selection in view after filtering + + + + + FindDialog + + + Find + + + + + Find what: + + + + + + Search term + + + + + + Find next occurence from current file position. + + + + + &Find Next + + + + + + + Close + + + + + QObject + + + Filter + + + + + One of the following plugins must be enabled: %1. + + + + + This plugin can only be enabled if the '%1' plugin is installed and enabled. + + + + + This plugin can only be enabled for the following game(s): %1. + + + + + + + + + + + INI file is read-only + + + + + + Mod Organizer is attempting to write to "%1" which is currently set to read-only. + + + + + + Clear the read-only flag + + + + + + Allow the write once + + + + + + The file will be set to read-only again. + + + + + + Skip this file + + + + + + Error + + + + + You can reset these choices by clicking "Reset Dialog Choices" in the General tab of the Settings + + + + + Always ask + + + + + + Remember my choice + + + + + Remember my choice for %1 + + + + + Failed to save '%1', could not create a temporary file: %2 (error %3) + Failed to save '{}', could not create a temporary file: {} (error {}) + + + + + removal of "%1" failed: %2 + + + + + removal of "%1" failed + + + + + "%1" doesn't exist (remove) + + + + + Error %1 + + + + + + You have an invalid custom browser command in the settings. + + + + + + failed to create directory "%1" + + + + + + failed to copy "%1" to "%2" + + + + + %1 B + + + + + %1 KB + + + + + %1 MB + + + + + %1 GB + + + + + %1 TB + + + + + %1 B/s + + + + + %1 KB/s + + + + + %1 MB/s + + + + + %1 GB/s + + + + + %1 TB/s + + + + + QuestionBoxMemory + + + Remember selection + + + + + Remember selection only for %1 + + + + + TaskDialog + + + Dialog + + + + + icon + + + + + dummy main text + + + + + dummy content text + + + + + dummy button + + + + + dummy checkbox + + + + + Details + + + + + TextViewer + + + Log Viewer + + + + + Placeholder + + + + + Show Whitespace + + + + + Save changes? + + + + + Do you want to save changes to %1? + + + + + failed to write to %1 + + + + + file not found: %1 + + + + + Save + + + + + TutorialControl + + + Tutorial failed to start, please check "mo_interface.log" for details. + + + + + TutorialManager + + + tutorial manager not set up yet + + + + + uibase + + + h + Time remaining hours + + + + + m + Time remaining minutes + + + + + s + Time remaining seconds + + + + diff --git a/libs/uibase/src/utility.cpp b/libs/uibase/src/utility.cpp new file mode 100644 index 0000000..0ad64ad --- /dev/null +++ b/libs/uibase/src/utility.cpp @@ -0,0 +1,1260 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#define FO_RECYCLE 0x1003 +#endif + +namespace MOBase +{ + +bool removeDir(const QString& dirName) +{ + QDir dir(dirName); + + if (dir.exists()) { + Q_FOREACH (QFileInfo info, + dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, + QDir::DirsFirst)) { + if (info.isDir()) { + if (!removeDir(info.absoluteFilePath())) { + return false; + } + } else { + // On Linux, just make sure file is writable before removing + QFile file(info.absoluteFilePath()); + file.setPermissions(file.permissions() | QFile::WriteUser); + if (!file.remove()) { + reportError(QObject::tr("removal of \"%1\" failed: %2") + .arg(info.absoluteFilePath()) + .arg(file.errorString())); + return false; + } + } + } + + if (!dir.rmdir(dirName)) { + reportError(QObject::tr("removal of \"%1\" failed").arg(dir.absolutePath())); + return false; + } + } else { + reportError(QObject::tr("\"%1\" doesn't exist (remove)").arg(dirName)); + return false; + } + + return true; +} + +bool copyDir(const QString& sourceName, const QString& destinationName, bool merge) +{ + QDir sourceDir(sourceName); + if (!sourceDir.exists()) { + return false; + } + QDir destDir(destinationName); + if (!destDir.exists()) { + destDir.mkdir(destinationName); + } else if (!merge) { + return false; + } + + QStringList files = sourceDir.entryList(QDir::Files); + foreach (QString fileName, files) { + QString srcName = sourceName + "/" + fileName; + QString destName = destinationName + "/" + fileName; + QFile::copy(srcName, destName); + } + + files.clear(); + // we leave out symlinks because that could cause an endless recursion + QStringList subDirs = + sourceDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); + foreach (QString subDir, subDirs) { + QString srcName = sourceName + "/" + subDir; + QString destName = destinationName + "/" + subDir; + copyDir(srcName, destName, merge); + } + return true; +} + +// Linux shell operations use QFile/QDir instead of SHFileOperation + +static bool shellOpCopy(const QStringList& sourceNames, + const QStringList& destinationNames) +{ + // Multiple sources → single destination: treat destination as a directory + if (destinationNames.count() == 1 && sourceNames.count() > 1) { + QDir destDir(destinationNames[0]); + if (!destDir.exists()) { + destDir.mkpath("."); + } + for (const auto& src : sourceNames) { + QFileInfo srcInfo(src); + QString dest = destinationNames[0] + "/" + srcInfo.fileName(); + if (!QFile::copy(src, dest)) { + return false; + } + } + return true; + } + + // 1:1 or N:N — direct file-to-file copy + if (destinationNames.count() != sourceNames.count()) { + return false; + } + + for (int i = 0; i < sourceNames.count(); ++i) { + QFileInfo destInfo(destinationNames[i]); + if (!destInfo.dir().exists()) { + destInfo.dir().mkpath("."); + } + if (!QFile::copy(sourceNames[i], destinationNames[i])) { + return false; + } + } + return true; +} + +static bool shellOpMove(const QStringList& sourceNames, + const QStringList& destinationNames) +{ + // Multiple sources → single destination: treat destination as a directory + if (destinationNames.count() == 1 && sourceNames.count() > 1) { + QDir destDir(destinationNames[0]); + if (!destDir.exists()) { + destDir.mkpath("."); + } + for (const auto& src : sourceNames) { + QFileInfo srcInfo(src); + QString dest = destinationNames[0] + "/" + srcInfo.fileName(); + if (!QFile::rename(src, dest)) { + if (!QFile::copy(src, dest) || !QFile::remove(src)) { + return false; + } + } + } + return true; + } + + // 1:1 or N:N — direct file-to-file move + if (destinationNames.count() != sourceNames.count()) { + return false; + } + + for (int i = 0; i < sourceNames.count(); ++i) { + QFileInfo destInfo(destinationNames[i]); + if (!destInfo.dir().exists()) { + destInfo.dir().mkpath("."); + } + if (!QFile::rename(sourceNames[i], destinationNames[i])) { + if (!QFile::copy(sourceNames[i], destinationNames[i]) || + !QFile::remove(sourceNames[i])) { + return false; + } + } + } + return true; +} + +static bool shellOpDelete(const QStringList& fileNames, bool recycle) +{ + // On Linux, "recycle" moves to trash using Qt; otherwise just delete + for (const auto& fileName : fileNames) { + QFileInfo fi(fileName); + if (fi.isDir()) { + if (!removeDir(fileName)) { + return false; + } + } else { + if (recycle) { + if (!QFile::moveToTrash(fileName)) { + return false; + } + } else { + if (!QFile::remove(fileName)) { + return false; + } + } + } + } + return true; +} + +bool shellCopy(const QStringList& sourceNames, const QStringList& destinationNames, + QWidget* dialog) +{ + (void)dialog; + return shellOpCopy(sourceNames, destinationNames); +} + +bool shellCopy(const QString& sourceNames, const QString& destinationNames, + bool yesToAll, QWidget* dialog) +{ + (void)yesToAll; + (void)dialog; + return shellOpCopy(QStringList() << sourceNames, QStringList() << destinationNames); +} + +bool shellMove(const QStringList& sourceNames, const QStringList& destinationNames, + QWidget* dialog) +{ + (void)dialog; + return shellOpMove(sourceNames, destinationNames); +} + +bool shellMove(const QString& sourceNames, const QString& destinationNames, + bool yesToAll, QWidget* dialog) +{ + (void)yesToAll; + (void)dialog; + return shellOpMove(QStringList() << sourceNames, QStringList() << destinationNames); +} + +bool shellRename(const QString& oldName, const QString& newName, bool yesToAll, + QWidget* dialog) +{ + (void)yesToAll; + (void)dialog; + return QFile::rename(oldName, newName); +} + +bool shellDelete(const QStringList& fileNames, bool recycle, QWidget* dialog) +{ + (void)dialog; + return shellOpDelete(fileNames, recycle); +} + +namespace shell +{ + + static QString g_urlHandler; + + Result::Result(bool success, DWORD error, QString message, HANDLE process) + : m_success(success), m_error(error), m_message(std::move(message)), + m_process(process) + { + if (m_message.isEmpty()) { + m_message = QString::fromStdWString(formatSystemMessage(m_error)); + } + } + + Result Result::makeFailure(DWORD error, QString message) + { + return Result(false, error, std::move(message), INVALID_HANDLE_VALUE); + } + + Result Result::makeSuccess(HANDLE process) + { + return Result(true, ERROR_SUCCESS, {}, process); + } + + bool Result::success() const + { + return m_success; + } + + Result::operator bool() const + { + return m_success; + } + + DWORD Result::error() + { + return m_error; + } + + const QString& Result::message() const + { + return m_message; + } + + HANDLE Result::processHandle() const + { + return m_process.get(); + } + + HANDLE Result::stealProcessHandle() + { + const auto h = m_process.release(); + m_process.reset(INVALID_HANDLE_VALUE); + return h; + } + + QString Result::toString() const + { + if (m_message.isEmpty()) { + return QObject::tr("Error %1").arg(m_error); + } else { + return m_message; + } + } + + QString formatError(int i) + { + switch (i) { + case 0: + return "The operating system is out of memory or resources"; + case (int)ERROR_FILE_NOT_FOUND: + return "The specified file was not found"; + case (int)ERROR_PATH_NOT_FOUND: + return "The specified path was not found"; + case (int)ERROR_BAD_FORMAT: + return "The .exe file is invalid (non-Win32 .exe or error in .exe image)"; + case SE_ERR_ACCESSDENIED: + return "The operating system denied access to the specified file"; + case SE_ERR_ASSOCINCOMPLETE: + return "The file name association is incomplete or invalid"; + case SE_ERR_DDEBUSY: + return "The DDE transaction could not be completed because other DDE " + "transactions were being processed"; + case SE_ERR_DDEFAIL: + return "The DDE transaction failed"; + case SE_ERR_DDETIMEOUT: + return "The DDE transaction could not be completed because the request " + "timed out"; + case SE_ERR_DLLNOTFOUND: + return "The specified DLL was not found"; + case SE_ERR_NOASSOC: + return "There is no application associated with the given file name " + "extension"; + case SE_ERR_OOM: + return "There was not enough memory to complete the operation"; + case SE_ERR_SHARE: + return "A sharing violation occurred"; + default: + return QString("Unknown error %1").arg(i); + } + } + + Result ExploreDirectory(const QFileInfo& info) + { + const auto path = info.absoluteFilePath(); + // Use xdg-open on Linux + if (QProcess::startDetached("xdg-open", {path})) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open directory")); + } + + Result ExploreFileInDirectory(const QFileInfo& info) + { + // Try to use the system file manager to highlight the file + // dbus method for Nautilus/Files, fallback to xdg-open on parent dir + const auto dir = info.absolutePath(); + if (QProcess::startDetached("xdg-open", {dir})) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open file manager")); + } + + Result Explore(const QFileInfo& info) + { + if (info.isFile()) { + return ExploreFileInDirectory(info); + } else if (info.isDir()) { + return ExploreDirectory(info); + } else { + const auto parent = info.dir(); + if (parent.exists()) { + return ExploreDirectory(QFileInfo(parent.absolutePath())); + } else { + return Result::makeFailure(ERROR_FILE_NOT_FOUND); + } + } + } + + Result Explore(const QString& path) + { + return Explore(QFileInfo(path)); + } + + Result Explore(const QDir& dir) + { + return Explore(QFileInfo(dir.absolutePath())); + } + + Result Open(const QString& path) + { + if (QProcess::startDetached("xdg-open", {path})) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open file")); + } + + Result Open(const QUrl& url) + { + log::debug("opening url '{}'", url.toString()); + + if (g_urlHandler.isEmpty()) { + if (QProcess::startDetached("xdg-open", {url.toString()})) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open URL")); + } else { + // Custom URL handler + QString cmd = g_urlHandler; + cmd.replace("%1", url.toString()); + if (QProcess::startDetached("/bin/sh", {"-c", cmd})) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_FILE_NOT_FOUND, + QObject::tr("You have an invalid custom browser command in the settings.")); + } + } + + Result Execute(const QString& program, const QString& params) + { + QStringList args; + if (!params.isEmpty()) { + args = QProcess::splitCommand(params); + } + if (QProcess::startDetached(program, args)) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to execute program")); + } + + void SetUrlHandler(const QString& cmd) + { + g_urlHandler = cmd; + } + + Result Delete(const QFileInfo& path) + { + if (QFile::remove(path.absoluteFilePath())) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_ACCESS_DENIED); + } + + Result Rename(const QFileInfo& src, const QFileInfo& dest) + { + return Rename(src, dest, true); + } + + Result Rename(const QFileInfo& src, const QFileInfo& dest, bool copyAllowed) + { + if (QFile::rename(src.absoluteFilePath(), dest.absoluteFilePath())) { + return Result::makeSuccess(); + } + + if (copyAllowed) { + if (QFile::copy(src.absoluteFilePath(), dest.absoluteFilePath())) { + QFile::remove(src.absoluteFilePath()); + return Result::makeSuccess(); + } + } + + return Result::makeFailure(ERROR_ACCESS_DENIED); + } + + Result CreateDirectories(const QDir& dir) + { + if (dir.mkpath(".")) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_ACCESS_DENIED, + QObject::tr("Failed to create directory: %1").arg(dir.path())); + } + + Result DeleteDirectoryRecursive(const QDir& dir) + { + if (shellOpDelete({dir.path()}, false)) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_ACCESS_DENIED); + } + +} // namespace shell + +bool moveFileRecursive(const QString& source, const QString& baseDir, + const QString& destination) +{ + QStringList pathComponents = destination.split("/"); + QString path = baseDir; + for (QStringList::Iterator iter = pathComponents.begin(); + iter != pathComponents.end() - 1; ++iter) { + path.append("/").append(*iter); + if (!QDir(path).exists() && !QDir().mkdir(path)) { + reportError(QObject::tr("failed to create directory \"%1\"").arg(path)); + return false; + } + } + + QString destinationAbsolute = baseDir.mid(0).append("/").append(destination); + if (!QFile::rename(source, destinationAbsolute)) { + // move failed, try copy & delete + if (!QFile::copy(source, destinationAbsolute)) { + reportError(QObject::tr("failed to copy \"%1\" to \"%2\"") + .arg(source) + .arg(destinationAbsolute)); + return false; + } else { + QFile::remove(source); + } + } + return true; +} + +bool copyFileRecursive(const QString& source, const QString& baseDir, + const QString& destination) +{ + QStringList pathComponents = destination.split("/"); + QString path = baseDir; + for (QStringList::Iterator iter = pathComponents.begin(); + iter != pathComponents.end() - 1; ++iter) { + path.append("/").append(*iter); + if (!QDir(path).exists() && !QDir().mkdir(path)) { + reportError(QObject::tr("failed to create directory \"%1\"").arg(path)); + return false; + } + } + + QString destinationAbsolute = baseDir.mid(0).append("/").append(destination); + if (!QFile::copy(source, destinationAbsolute)) { + reportError(QObject::tr("failed to copy \"%1\" to \"%2\"") + .arg(source) + .arg(destinationAbsolute)); + return false; + } + return true; +} + +std::wstring ToWString(const QString& source) +{ + return source.toStdWString(); +} + +std::string ToString(const QString& source, bool utf8) +{ + QByteArray array8bit; + if (utf8) { + array8bit = source.toUtf8(); + } else { + array8bit = source.toLocal8Bit(); + } + return std::string(array8bit.constData()); +} + +QString ToQString(const std::string& source) +{ + return QString::fromStdString(source); +} + +QString ToQString(const std::wstring& source) +{ + return QString::fromStdWString(source); +} + +bool isWindowsDrivePath(const QString& path) +{ + static const QRegularExpression re(R"(^\s*[A-Za-z]:[\\/].*)"); + return re.match(path).hasMatch(); +} + +bool isWineZDrivePath(const QString& path) +{ + static const QRegularExpression re(R"(^\s*[Zz]:[\\/].*)"); + return re.match(path).hasMatch(); +} + +QString toWinePath(const QString& path) +{ +#ifdef _WIN32 + return QDir::toNativeSeparators(path); +#else + QString p = path.trimmed(); + if (p.isEmpty()) { + return p; + } + + if (isWindowsDrivePath(p)) { + p.replace('/', '\\'); + return p; + } + + // Expand "~" on Linux before mapping to Z:. + if (p.startsWith("~")) { + p.replace(0, 1, QDir::homePath()); + } + + if (!QDir::isAbsolutePath(p)) { + p = QDir(p).absolutePath(); + } + + p = QDir::cleanPath(p); + p.replace('/', '\\'); + return "Z:" + p; +#endif +} + +QString fromWinePath(const QString& path) +{ +#ifdef _WIN32 + return QDir::toNativeSeparators(path); +#else + QString p = path.trimmed(); + if (p.isEmpty()) { + return p; + } + + if (!isWindowsDrivePath(p)) { + return QDir::cleanPath(p); + } + + // We only map Wine's Z: drive to host Linux absolute paths. + if (!isWineZDrivePath(p)) { + return p; + } + + p = p.mid(2); // strip "Z:" + p.replace('\\', '/'); + if (!p.startsWith('/')) { + p.prepend('/'); + } + + return QDir::cleanPath(p); +#endif +} + +QString normalizePathForHost(const QString& path) +{ +#ifdef _WIN32 + return QDir::toNativeSeparators(path); +#else + if (isWineZDrivePath(path)) { + return fromWinePath(path); + } + return QDir::cleanPath(path); +#endif +} + +QString normalizePathForWine(const QString& path) +{ +#ifdef _WIN32 + return QDir::toNativeSeparators(path); +#else + if (isWindowsDrivePath(path)) { + QString p = path; + p.replace('/', '\\'); + return p; + } + return toWinePath(path); +#endif +} + +#ifdef _WIN32 +QString ToString(const SYSTEMTIME& time) +{ + char dateBuffer[100]; + char timeBuffer[100]; + int size = 100; + GetDateFormatA(LOCALE_USER_DEFAULT, LOCALE_USE_CP_ACP, &time, nullptr, dateBuffer, + size); + GetTimeFormatA(LOCALE_USER_DEFAULT, LOCALE_USE_CP_ACP, &time, nullptr, timeBuffer, + size); + return QString::fromLocal8Bit(dateBuffer) + " " + QString::fromLocal8Bit(timeBuffer); +} +#endif + +static int naturalCompareI(const QString& a, const QString& b) +{ + static QCollator c = [] { + QCollator temp; + temp.setNumericMode(true); + temp.setCaseSensitivity(Qt::CaseInsensitive); + return temp; + }(); + + return c.compare(a, b); +} + +int naturalCompare(const QString& a, const QString& b, Qt::CaseSensitivity cs) +{ + if (cs == Qt::CaseInsensitive) { + return naturalCompareI(a, b); + } + + static QCollator c = [] { + QCollator temp; + temp.setNumericMode(true); + return temp; + }(); + + return c.compare(a, b); +} + +#ifdef _WIN32 +struct CoTaskMemFreer +{ + void operator()(void* p) { ::CoTaskMemFree(p); } +}; + +template +using COMMemPtr = std::unique_ptr; + +QString getOptionalKnownFolder(KNOWNFOLDERID id) +{ + COMMemPtr path; + { + wchar_t* rawPath = nullptr; + HRESULT res = SHGetKnownFolderPath(id, 0, nullptr, &rawPath); + if (FAILED(res)) { + return {}; + } + path.reset(rawPath); + } + return QString::fromWCharArray(path.get()); +} + +QDir getKnownFolder(KNOWNFOLDERID id, const QString& what) +{ + COMMemPtr path; + { + wchar_t* rawPath = nullptr; + HRESULT res = SHGetKnownFolderPath(id, 0, nullptr, &rawPath); + if (FAILED(res)) { + log::error("failed to get known folder '{}', {}", + what.isEmpty() ? QUuid(id).toString() : what, + formatSystemMessage(res)); + throw std::runtime_error("couldn't get known folder path"); + } + path.reset(rawPath); + } + return QString::fromWCharArray(path.get()); +} +#endif + +QString getDesktopDirectory() +{ +#ifdef _WIN32 + return getKnownFolder(FOLDERID_Desktop, "desktop").absolutePath(); +#else + return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); +#endif +} + +QString getStartMenuDirectory() +{ +#ifdef _WIN32 + return getKnownFolder(FOLDERID_StartMenu, "start menu").absolutePath(); +#else + return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); +#endif +} + +bool shellDeleteQuiet(const QString& fileName, QWidget* dialog) +{ + if (!QFile::remove(fileName)) { + return shellDelete(QStringList(fileName), false, dialog); + } + return true; +} + +QString readFileText(const QString& fileName, QString* encoding, bool* hadBOM) +{ + + QFile textFile(fileName); + if (!textFile.open(QIODevice::ReadOnly)) { + return QString(); + } + + QByteArray buffer = textFile.readAll(); + return decodeTextData(buffer, encoding, hadBOM); +} + +QString decodeTextData(const QByteArray& fileData, QString* encoding, bool* hadBOM) +{ + QStringConverter::Encoding codec = QStringConverter::Encoding::Utf8; + QStringEncoder encoder(codec); + QStringDecoder decoder(codec, QStringConverter::Flag::ConvertInitialBom); + QString text = decoder.decode(fileData); + + // embedded nulls probably mean it was UTF-16 - they're rare/illegal in text files + bool hasEmbeddedNulls = false; + for (const auto& character : text) { + if (character.isNull()) { + hasEmbeddedNulls = true; + break; + } + } + + // check reverse conversion + if (hasEmbeddedNulls || encoder.encode(text) != fileData) { + log::debug("conversion failed assuming local encoding"); + auto codecSearch = QStringConverter::encodingForData(fileData); + if (codecSearch.has_value()) { + codec = codecSearch.value(); + decoder = QStringDecoder(codec, QStringConverter::Flag::ConvertInitialBom); + } else { + decoder = QStringDecoder(hasEmbeddedNulls ? QStringConverter::Encoding::Utf16 + : QStringConverter::Encoding::System); + } + text = decoder.decode(fileData); + } + + if (encoding != nullptr) { + *encoding = QStringConverter::nameForEncoding(codec); + } + + if (!text.isEmpty() && text.startsWith(QChar::ByteOrderMark)) { + text.remove(0, 1); + + if (hadBOM != nullptr) { + *hadBOM = true; + } + } else if (hadBOM != nullptr) { + *hadBOM = false; + } + + return text; +} + +void removeOldFiles(const QString& path, const QString& pattern, int numToKeep, + QDir::SortFlags sorting) +{ + QFileInfoList files = + QDir(path).entryInfoList(QStringList(pattern), QDir::Files, sorting); + + if (files.count() > numToKeep) { + QStringList deleteFiles; + for (int i = 0; i < files.count() - numToKeep; ++i) { + deleteFiles.append(files.at(i).absoluteFilePath()); + } + + if (!shellDelete(deleteFiles)) { + log::warn("failed to remove log files"); + } + } +} + +QIcon iconForExecutable(const QString& filePath) +{ + static QHash cache; + + const QFileInfo fi(filePath); + if (!fi.exists()) { + return QIcon(":/MO/gui/executable"); + } + + const QString cacheKey = + fi.canonicalFilePath() + "|" + QString::number(fi.size()) + "|" + + fi.lastModified().toString(Qt::ISODateWithMs); + + if (cache.contains(cacheKey)) { + return cache.value(cacheKey); + } + +#ifdef _WIN32 + QIcon icon(":/MO/gui/executable"); + cache.insert(cacheKey, icon); + return icon; +#else + // Try to extract icon resources from PE executables via icoutils/wrestool. + // If this fails, return a generic executable icon. + QIcon icon; + + const QString wrestool = QStandardPaths::findExecutable("wrestool"); + if (!wrestool.isEmpty()) { + const QString cacheRoot = + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + "/exe_icons"; + QDir().mkpath(cacheRoot); + + const QString outDirPath = cacheRoot + "/" + QString::number(qHash(cacheKey), 16); + QDir outDir(outDirPath); + if (!outDir.exists()) { + QDir().mkpath(outDirPath); + } + + auto findBestIcon = [&](const QString& dirPath) -> QIcon { + QString bestFile; + int bestArea = -1; + + QDirIterator it(dirPath, + QStringList() << "*.png" + << "*.ico" + << "*.bmp", + QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + const QString path = it.next(); + QImageReader reader(path); + const QSize size = reader.size(); + const int area = size.width() * size.height(); + if (area > bestArea) { + bestArea = area; + bestFile = path; + } else if (bestArea < 0) { + // Unknown image size (e.g. multi-size ico); keep first viable file. + bestFile = path; + } + } + + if (!bestFile.isEmpty()) { + QIcon candidate(bestFile); + if (!candidate.pixmap(24, 24).isNull() || !candidate.pixmap(32, 32).isNull()) { + return candidate; + } + } + + return QIcon(); + }; + + // Reuse previous extraction if present. + icon = findBestIcon(outDirPath); + + if (icon.isNull()) { + const QList tries = { + {"-x", "--type=group_icon", "-o", outDirPath, fi.absoluteFilePath()}, + {"-x", "--type=14", "-o", outDirPath, fi.absoluteFilePath()}, + {"-x", "-t14", "-o", outDirPath, fi.absoluteFilePath()}, + }; + + for (const auto& args : tries) { + QProcess p; + p.start(wrestool, args); + if (!p.waitForFinished(4000)) { + p.kill(); + continue; + } + if (p.exitStatus() == QProcess::NormalExit && p.exitCode() == 0) { + icon = findBestIcon(outDirPath); + if (!icon.isNull()) { + break; + } + } + } + } + } + + if (icon.isNull()) { + icon = QIcon(":/MO/gui/executable"); + } + + cache.insert(cacheKey, icon); + return icon; +#endif +} + +QString getFileVersion(QString const& filepath) +{ + // On Linux, there is no Windows PE file version. Return empty. + (void)filepath; + return ""; +} + +QString getProductVersion(QString const& filepath) +{ + // On Linux, there is no Windows PE product version. Return empty. + (void)filepath; + return ""; +} + +void deleteChildWidgets(QWidget* w) +{ + auto* ly = w->layout(); + if (!ly) { + return; + } + + while (auto* item = ly->takeAt(0)) { + delete item->widget(); + delete item; + } +} + +void trimWString(std::wstring& s) +{ + s.erase(std::remove_if(s.begin(), s.end(), + [](wint_t ch) { + return std::iswspace(ch); + }), + s.end()); +} + +std::wstring formatMessage(DWORD id, const std::wstring& message) +{ + std::wstring s; + + std::wostringstream oss; + oss << L"0x" << std::hex << id; + + if (message.empty()) { + s = oss.str(); + } else { + s += message + L" (" + oss.str() + L")"; + } + + return s; +} + +std::wstring formatSystemMessage(DWORD id) +{ +#ifdef _WIN32 + std::wstring getMessage(DWORD id, HMODULE mod); + return formatMessage(id, getMessage(id, 0)); +#else + // On Linux, map common error codes to strings or use strerror for errno values + if (id == 0) { + return L"Success"; + } + // If it looks like an errno value (small numbers), use strerror + if (id < 200) { + const char* msg = strerror(static_cast(id)); + if (msg) { + std::string s(msg); + return std::wstring(s.begin(), s.end()); + } + } + return formatMessage(id, L""); +#endif +} + +std::wstring formatNtMessage(NTSTATUS s) +{ +#ifdef _WIN32 + const DWORD id = static_cast(s); + std::wstring getMessage(DWORD id, HMODULE mod); + return formatMessage(id, getMessage(id, ::GetModuleHandleW(L"ntdll.dll"))); +#else + return formatMessage(static_cast(s), L"NT status"); +#endif +} + +QString windowsErrorString(DWORD errorCode) +{ + return QString::fromStdWString(formatSystemMessage(errorCode)); +} + +QString localizedSize(unsigned long long bytes, const QString& B, const QString& KB, + const QString& MB, const QString& GB, const QString& TB) +{ + constexpr unsigned long long OneKB = 1024ull; + constexpr unsigned long long OneMB = 1024ull * 1024; + constexpr unsigned long long OneGB = 1024ull * 1024 * 1024; + constexpr unsigned long long OneTB = 1024ull * 1024 * 1024 * 1024; + + auto makeNum = [&](int factor) { + const double n = static_cast(bytes) / std::pow(1024.0, factor); + const double truncated = + static_cast(static_cast(n * 100)) / 100.0; + return QString().setNum(truncated, 'f', 2); + }; + + if (bytes < OneKB) { + return B.arg(bytes); + } else if (bytes < OneMB) { + return KB.arg(makeNum(1)); + } else if (bytes < OneGB) { + return MB.arg(makeNum(2)); + } else if (bytes < OneTB) { + return GB.arg(makeNum(3)); + } else { + return TB.arg(makeNum(4)); + } +} + +QDLLEXPORT QString localizedByteSize(unsigned long long bytes) +{ + return localizedSize(bytes, QObject::tr("%1 B"), QObject::tr("%1 KB"), + QObject::tr("%1 MB"), QObject::tr("%1 GB"), + QObject::tr("%1 TB")); +} + +QDLLEXPORT QString localizedByteSpeed(unsigned long long bps) +{ + return localizedSize(bps, QObject::tr("%1 B/s"), QObject::tr("%1 KB/s"), + QObject::tr("%1 MB/s"), QObject::tr("%1 GB/s"), + QObject::tr("%1 TB/s")); +} + +QDLLEXPORT QString localizedTimeRemaining(unsigned int remaining) +{ + QString Result; + double interval; + qint64 intval; + + // Hours + interval = 60.0 * 60.0 * 1000.0; + intval = (qint64)trunc((double)remaining / interval); + if (intval < 0) + intval = 0; + remaining -= static_cast(trunc((double)intval * interval)); + qint64 hours = intval; + + // Minutes + interval = 60.0 * 1000.0; + intval = (qint64)trunc((double)remaining / interval); + if (intval < 0) + intval = 0; + remaining -= static_cast(trunc((double)intval * interval)); + qint64 minutes = intval; + + // Seconds + interval = 1000.0; + intval = (qint64)trunc((double)remaining / interval); + if (intval < 0) + intval = 0; + remaining -= static_cast(trunc((double)intval * interval)); + qint64 seconds = intval; + + char buffer[25]; + memset(buffer, 0, 25); + + if (hours > 0) { + if (hours < 10) + snprintf(buffer, sizeof(buffer), "0%lld", (long long)hours); + else + snprintf(buffer, sizeof(buffer), "%lld", (long long)hours); + Result.append(QString("%1:").arg(buffer)); + } + + if (minutes > 0 || hours > 0) { + if (minutes < 10 && hours > 0) + snprintf(buffer, sizeof(buffer), "0%lld", (long long)minutes); + else + snprintf(buffer, sizeof(buffer), "%lld", (long long)minutes); + Result.append(QString("%1:").arg(buffer)); + } + + if (seconds < 10 && (minutes > 0 || hours > 0)) + snprintf(buffer, sizeof(buffer), "0%lld", (long long)seconds); + else + snprintf(buffer, sizeof(buffer), "%lld", (long long)seconds); + Result.append(QString("%1").arg(buffer)); + + if (hours > 0) + //: Time remaining hours + Result.append(QApplication::translate("uibase", "h")); + else if (minutes > 0) + //: Time remaining minutes + Result.append(QApplication::translate("uibase", "m")); + else + //: Time remaining seconds + Result.append(QApplication::translate("uibase", "s")); + + return Result; +} + +QDLLEXPORT void localizedByteSizeTests() +{ + auto f = [](unsigned long long n) { + return localizedByteSize(n).toStdString(); + }; + +#define CHECK_EQ(a, b) \ + if ((a) != (b)) { \ + std::cerr << "failed: " << a << " == " << b << "\n"; \ + } + + CHECK_EQ(f(0), "0 B"); + CHECK_EQ(f(1), "1 B"); + CHECK_EQ(f(999), "999 B"); + CHECK_EQ(f(1000), "1000 B"); + CHECK_EQ(f(1023), "1023 B"); + + CHECK_EQ(f(1024), "1.00 KB"); + CHECK_EQ(f(2047), "1.99 KB"); + CHECK_EQ(f(2048), "2.00 KB"); + CHECK_EQ(f(1048575), "1023.99 KB"); + + CHECK_EQ(f(1048576), "1.00 MB"); + CHECK_EQ(f(1073741823), "1023.99 MB"); + + CHECK_EQ(f(1073741824), "1.00 GB"); + CHECK_EQ(f(1099511627775), "1023.99 GB"); + + CHECK_EQ(f(1099511627776), "1.00 TB"); + CHECK_EQ(f(2759774185818), "2.51 TB"); + +#undef CHECK_EQ +} + +TimeThis::TimeThis(const QString& what) : m_running(false) +{ + start(what); +} + +TimeThis::~TimeThis() +{ + stop(); +} + +void TimeThis::start(const QString& what) +{ + stop(); + + m_what = what; + m_start = Clock::now(); + m_running = true; +} + +void TimeThis::stop() +{ + using namespace std::chrono; + + if (!m_running) { + return; + } + + const auto end = Clock::now(); + const auto d = duration_cast(end - m_start).count(); + + if (m_what.isEmpty()) { + log::debug("timing: {} ms", d); + } else { + log::debug("timing: {} {} ms", m_what, d); + } + + m_running = false; +} + +} // namespace MOBase diff --git a/libs/uibase/src/version.rc b/libs/uibase/src/version.rc new file mode 100644 index 0000000..fdea5f9 --- /dev/null +++ b/libs/uibase/src/version.rc @@ -0,0 +1,37 @@ +#include "Winver.h" + +// If VS_FF_PRERELEASE is not set, MO labels the build as a release and uses VER_FILEVERSION to determine version number. +// Otherwise, if letters are used in VER_FILEVERSION_STR, uses the full MOBase::VersionInfo parser +// Otherwise, uses the numbers from VER_FILEVERSION and sets the release type as pre-alpha +#define VER_FILEVERSION 2.5.3 +#define VER_FILEVERSION_STR "2.5.3\0" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION VER_FILEVERSION +PRODUCTVERSION VER_FILEVERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS (0) +FILEOS VOS__WINDOWS32 +FILETYPE VFT_APP +FILESUBTYPE (0) +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "CompanyName", "Mod Organizer 2 Team\0" + VALUE "FileDescription", "MO2 Base UI plugin API library\0" + VALUE "OriginalFilename", "uibase.dll\0" + VALUE "InternalName", "uibase\0" + VALUE "LegalCopyright", "Copyright 2011-2016 Sebastian Herbord\r\nCopyright 2016-2026 Mod Organizer 2 contributors\0" + VALUE "ProductName", "Mod Organizer 2 UI Base\0" + VALUE "ProductVersion", VER_FILEVERSION_STR + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1200 + END +END diff --git a/libs/uibase/src/versioninfo.cpp b/libs/uibase/src/versioninfo.cpp new file mode 100644 index 0000000..6dec18e --- /dev/null +++ b/libs/uibase/src/versioninfo.cpp @@ -0,0 +1,432 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 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 +#include +#include +#include +#include + +namespace +{ +const QRegularExpression VERSION_REGEX("^(\\d+)(\\.(\\d+))?(\\.(\\d+))?(\\.(\\d+))?"); +} + +namespace MOBase +{ + +VersionInfo::VersionInfo() + : m_Scheme(SCHEME_REGULAR), m_Valid(false), m_ReleaseType(RELEASE_FINAL), + m_Major(0), m_Minor(0), m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0), + m_Rest() +{} + +VersionInfo::VersionInfo(int major, int minor, int subminor, int subsubminor, + ReleaseType releaseType) + : m_Scheme(SCHEME_REGULAR), m_Valid(true), m_ReleaseType(releaseType), + m_Major(major), m_Minor(minor), m_SubMinor(subminor), m_SubSubMinor(subsubminor), + m_DecimalPositions(0), m_Rest() +{} + +VersionInfo::VersionInfo(int major, int minor, int subminor, ReleaseType releaseType) + : m_Scheme(SCHEME_REGULAR), m_Valid(true), m_ReleaseType(releaseType), + m_Major(major), m_Minor(minor), m_SubMinor(subminor), m_SubSubMinor(0), + m_DecimalPositions(0), m_Rest() +{} + +VersionInfo::VersionInfo(const QString& versionString, VersionScheme scheme) + : m_Valid(true), m_ReleaseType(RELEASE_FINAL), m_Major(0), m_Minor(0), + m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0), m_Rest() +{ + parse(versionString, scheme); +} + +VersionInfo::VersionInfo(const QString& versionString, + VersionInfo::VersionScheme scheme, bool manualInput) + : m_Valid(true), m_ReleaseType(RELEASE_FINAL), m_Major(0), m_Minor(0), + m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0), m_Rest() +{ + parse(versionString, scheme, manualInput); +} + +void VersionInfo::clear() +{ + m_Scheme = SCHEME_REGULAR; + m_Valid = false; + m_ReleaseType = RELEASE_FINAL; + m_Major = m_Minor = m_SubMinor = m_SubSubMinor = m_DecimalPositions = 0; + m_Rest.clear(); +} + +QString VersionInfo::canonicalString() const +{ + if (!isValid()) { + return QString(); + } + + QString result; + if (m_Scheme == SCHEME_REGULAR) { + result = QString("%1.%2.%3.%4") + .arg(m_Major) + .arg(m_Minor) + .arg(m_SubMinor) + .arg(m_SubSubMinor); + } else if (m_Scheme == SCHEME_DECIMALMARK) { + result = QString("f%1.%2").arg(m_Major).arg( + QString("%1").arg(m_Minor).rightJustified(m_DecimalPositions, '0')); + } else if (m_Scheme == SCHEME_NUMBERSANDLETTERS) { + result = QString("n%1.%2.%3.%4") + .arg(m_Major) + .arg(m_Minor) + .arg(m_SubMinor) + .arg(m_SubSubMinor); + } else if (m_Scheme == SCHEME_DATE) { + // year.month.day was stored in the version fields + result = QString("d%1.%2.%3.%4") + .arg(m_Major) + .arg(m_Minor) + .arg(m_SubMinor) + .arg(m_SubSubMinor); + } + switch (m_ReleaseType) { + case RELEASE_PREALPHA: { + result.append(" pre-alpha"); + } break; + case RELEASE_ALPHA: { + result.append("a"); + } break; + case RELEASE_BETA: { + result.append("b"); + } break; + case RELEASE_CANDIDATE: { + result.append("rc"); + } break; + case RELEASE_FINAL: // fall-through + default: { + // nop + } break; + } + + if (!m_Rest.isEmpty()) { + result.append(QString("%1").arg(m_Rest)); + } + + return result; +} + +QString VersionInfo::displayString(int forcedVersionSegments) const +{ + if (!isValid()) { + return QString(); + } + + QString result; + if (m_Scheme == SCHEME_REGULAR) { + if (forcedVersionSegments >= 4 || m_SubSubMinor != 0) { + result = QString("%1.%2.%3.%4") + .arg(m_Major) + .arg(m_Minor) + .arg(m_SubMinor) + .arg(m_SubSubMinor); + } else if (forcedVersionSegments == 3 || m_SubMinor != 0) { + result = QString("%1.%2.%3").arg(m_Major).arg(m_Minor).arg(m_SubMinor); + } else { + result = QString("%1.%2").arg(m_Major).arg(m_Minor); + } + } else if (m_Scheme == SCHEME_DECIMALMARK) { + result = QString("%1.%2").arg(m_Major).arg( + QString("%1").arg(m_Minor).rightJustified(m_DecimalPositions, '0')); + } else if (m_Scheme == SCHEME_NUMBERSANDLETTERS) { + result = QString("%1.%2.%3.%4") + .arg(m_Major) + .arg(m_Minor) + .arg(m_SubMinor) + .arg(m_SubSubMinor); + } else if (m_Scheme == SCHEME_DATE) { + // year.month.day was stored in the version fields + const auto year = m_Major; + const auto month = m_Minor; + const auto day = m_SubMinor; + + return QLocale::system().toString(QDate(year, month, day), + QLocale::FormatType::ShortFormat); + } + switch (m_ReleaseType) { + case RELEASE_PREALPHA: { + result.append(" pre-alpha"); + } break; + case RELEASE_ALPHA: { + result.append("alpha"); + } break; + case RELEASE_BETA: { + result.append("beta"); + } break; + case RELEASE_CANDIDATE: { + result.append("rc"); + } break; + case RELEASE_FINAL: // fall-through + default: { + // nop + } break; + } + + if (!m_Rest.isEmpty()) { + result.append(QString("%1").arg(m_Rest)); + } + + return result; +} + +QVersionNumber VersionInfo::asQVersionNumber() const +{ + return QVersionNumber::fromString(displayString()).normalized(); +} + +QString VersionInfo::parseReleaseType(QString versionString) +{ + // release types are often followed by a number (i.e. "beta4"). This needs to be + // extracted now, otherwise the outer parser will think it's the subminor version and + // then 1.0.0rc1 would be interpreted as newer than 1.0.0 + + static std::map typeStrings = {{"prealpha", RELEASE_PREALPHA}, + {"alpha", RELEASE_ALPHA}, + {"beta", RELEASE_BETA}, + {"rc", RELEASE_CANDIDATE}}; + + m_ReleaseType = RELEASE_FINAL; + + auto typeIter = typeStrings.begin(); + int offset = -1; + + for (; (typeIter != typeStrings.end()) && (offset == -1); ++typeIter) { + offset = (int)versionString.indexOf(typeIter->first, Qt::CaseInsensitive); + if (offset != -1) { + m_ReleaseType = typeIter->second; + break; + } + } + + int length = 0; + + if (typeIter != typeStrings.end()) { + length = (int)typeIter->first.length(); + } + + if (m_Scheme == SCHEME_REGULAR) { + // also interpret the a/b letters, but only if they follow immediately on the + // version number, otherwise the margin for error is too big + if ((offset == -1) && (versionString.length() > 0)) { + if (versionString.at(0) == 'a') { + m_ReleaseType = RELEASE_ALPHA; + offset = 0; + length = 1; + } else if (versionString.at(0) == 'b') { + m_ReleaseType = RELEASE_BETA; + offset = 0; + length = 1; + } + } + } + + if (offset != -1) { + versionString.remove(offset, length); + } + return versionString.trimmed(); +} + +void VersionInfo::parse(const QString& versionString, VersionScheme scheme, + bool manualInput) +{ + m_Valid = false; + m_Scheme = scheme == SCHEME_LITERAL ? SCHEME_REGULAR + : scheme != SCHEME_DISCOVER ? scheme + : SCHEME_REGULAR; + m_ReleaseType = RELEASE_FINAL; + m_Major = m_Minor = m_SubMinor = m_SubSubMinor = 0; + m_Rest.clear(); + if (versionString.length() == 0) { + return; + } + + if (QString::compare(versionString, "final", Qt::CaseInsensitive) == 0) { + m_Major = 1; + m_Valid = true; + return; + } + + QString temp = versionString; + // first, determine the versioning scheme if there is a hint + VersionScheme newScheme = m_Scheme; + if (!manualInput) { + if (temp.startsWith('f')) { + newScheme = SCHEME_DECIMALMARK; + temp.remove(0, 1); + } else if (temp.startsWith('n')) { + newScheme = SCHEME_NUMBERSANDLETTERS; + temp.remove(0, 1); + } else if (temp.startsWith('d')) { + newScheme = SCHEME_DATE; + temp.remove(0, 1); + } + } + + if (scheme == SCHEME_DISCOVER) { + m_Scheme = newScheme; + } + + if (temp.startsWith('v', Qt::CaseInsensitive)) { + // v is often prepended to versions + temp.remove(0, 1); + } + + auto match = VERSION_REGEX.match(temp); + if (match.hasMatch()) { + m_Major = match.captured(1).toInt(); + m_Minor = match.captured(3).toInt(); + QString subMinor = match.captured(5); + QString subSubMinor = match.captured(7); + if (!subMinor.isEmpty() && (m_Scheme == SCHEME_DECIMALMARK)) { + // nooooope, if there are two dots it can't be a decimal mark + m_Scheme = SCHEME_REGULAR; + } + if (m_Scheme != SCHEME_DECIMALMARK) { + m_SubMinor = subMinor.toInt(); + m_SubSubMinor = subSubMinor.toInt(); + } + if (subMinor.isEmpty() && (match.captured(3).size() > 1) && + match.captured(3).startsWith('0')) { + // this indicates a decimal scheme + m_Scheme = SCHEME_DECIMALMARK; + m_DecimalPositions = (int)match.captured(3).size(); + } + temp.remove(VERSION_REGEX); + } else { + m_Scheme = SCHEME_LITERAL; + } + + if (m_Scheme == SCHEME_REGULAR) { + temp = parseReleaseType(temp); + } + + if ((m_Scheme == SCHEME_DATE) && (m_Major < 1900)) { + m_Scheme = SCHEME_REGULAR; + } + m_Rest = temp.trimmed(); + m_Valid = true; +} + +QDLLEXPORT bool operator<(const VersionInfo& LHS, const VersionInfo& RHS) +{ + if (!LHS.isValid() && RHS.isValid()) + return true; + if (!RHS.isValid() && LHS.isValid()) + return false; + + // date-releases are lower than regular versions + if ((LHS.m_Scheme == VersionInfo::SCHEME_DATE) && + (RHS.m_Scheme != VersionInfo::SCHEME_DATE)) { + return true; + } else if ((LHS.m_Scheme != VersionInfo::SCHEME_DATE) && + (RHS.m_Scheme == VersionInfo::SCHEME_DATE)) { + return false; + } else if ((LHS.m_Scheme == VersionInfo::SCHEME_DECIMALMARK) || + (RHS.m_Scheme == VersionInfo::SCHEME_DECIMALMARK)) { + // use decimal versioning if either version is a decimal. The parser interprets + // versions as regular if in doubt so if the scheme is "decimal" it is definitively + // a decimal version number whereas SCHEME_REGULAR means "probably regular" + + float leftVal = QString("%1.%2") + .arg(LHS.m_Major) + .arg(QString("%1") + .arg(LHS.m_Minor) + .rightJustified(LHS.m_DecimalPositions, '0')) + .toFloat(); + float rightVal = QString("%1.%2") + .arg(RHS.m_Major) + .arg(QString("%1") + .arg(RHS.m_Minor) + .rightJustified(RHS.m_DecimalPositions, '0')) + .toFloat(); + if (fabs(leftVal - rightVal) > 0.001f) { + return leftVal < rightVal; + } + } else { + // if in doubt, use the sane choice. regular and numbers+letters can be treated the + // same way + if (LHS.m_Major != RHS.m_Major) + return LHS.m_Major < RHS.m_Major; + if (LHS.m_Minor != RHS.m_Minor) + return LHS.m_Minor < RHS.m_Minor; + if (LHS.m_SubMinor != RHS.m_SubMinor) + return LHS.m_SubMinor < RHS.m_SubMinor; + if (LHS.m_SubSubMinor != RHS.m_SubSubMinor) + return LHS.m_SubSubMinor < RHS.m_SubSubMinor; + } + + // subminor, release-type and rest are treated the same for all versioning schemes, + // but on parsing they may still differ, i.e. a b-suffix is only interpreted to mean + // "beta" in the regular scheme + if (LHS.m_ReleaseType != RHS.m_ReleaseType) + return LHS.m_ReleaseType < RHS.m_ReleaseType; + + // if the rest contains only integers, compare them numerically + bool LHS_ok, RHS_ok; + int LHS_int, RHS_int; + LHS_int = LHS.m_Rest.toInt(&LHS_ok); + RHS_int = RHS.m_Rest.toInt(&RHS_ok); + if (LHS_ok && RHS_ok) { + return LHS_int < RHS_int; + } + + // give up and compare lexically + return LHS.m_Rest < RHS.m_Rest; +} + +QDLLEXPORT bool operator>(const VersionInfo& LHS, const VersionInfo& RHS) +{ + return !(LHS <= RHS); +} + +QDLLEXPORT bool operator<=(const VersionInfo& LHS, const VersionInfo& RHS) +{ + // TODO not exactly optimized... + if (LHS < RHS) { + return true; + } else { + return !(RHS < LHS); + } +} + +QDLLEXPORT bool operator>=(const VersionInfo& LHS, const VersionInfo& RHS) +{ + return RHS <= LHS; +} + +QDLLEXPORT bool operator!=(const VersionInfo& LHS, const VersionInfo& RHS) +{ + return (LHS < RHS) || (RHS < LHS); +} + +QDLLEXPORT bool operator==(const VersionInfo& LHS, const VersionInfo& RHS) +{ + return !(LHS < RHS) && !(RHS < LHS); +} + +} // namespace MOBase diff --git a/libs/uibase/src/versioning.cpp b/libs/uibase/src/versioning.cpp new file mode 100644 index 0000000..8178841 --- /dev/null +++ b/libs/uibase/src/versioning.cpp @@ -0,0 +1,278 @@ +#include + +#include +#include +#include + +#include + +// official semver regex +static const QRegularExpression s_SemVerStrictRegEx{ + R"(^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"}; + +// for MO2, to match stuff like 1.2.3rc1 or v1.2.3a1+XXX +static const QRegularExpression s_SemVerMO2RegEx{ + R"(^v?(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:\.(?P0|[1-9]\d*))?(?:(?Pdev|a|alpha|b|beta|rc)(?P0|[1-9](?:[.0-9])*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"}; + +// match from value to release type +static const std::unordered_map + s_StringToRelease{{"dev", MOBase::Version::Development}, + {"alpha", MOBase::Version::Alpha}, + {"a", MOBase::Version::Alpha}, + {"beta", MOBase::Version::Beta}, + {"b", MOBase::Version::Beta}, + {"rc", MOBase::Version::ReleaseCandidate}}; + +namespace MOBase +{ + +namespace +{ + + Version parseVersionSemVer(QString const& value) + { + const auto match = s_SemVerStrictRegEx.match(value); + + if (!match.hasMatch()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid version string: '{}'", value))); + } + + const auto major = match.captured("major").toInt(); + const auto minor = match.captured("minor").toInt(); + const auto patch = match.captured("patch").toInt(); + + std::vector> prereleases; + for (auto& part : match.captured("prerelease") + .split(".", Qt::SplitBehaviorFlags::SkipEmptyParts)) { + // try to extract an int + bool ok = true; + const auto intValue = part.toInt(&ok); + if (ok) { + prereleases.push_back(intValue); + continue; + } + + // check if we have a valid prerelease type + const auto it = s_StringToRelease.find(part.toLower()); + if (it == s_StringToRelease.end()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid prerelease type: '{}'", part))); + } + + prereleases.push_back(it->second); + } + + const auto buildMetadata = match.captured("buildmetadata").trimmed(); + + return Version(major, minor, patch, 0, prereleases, buildMetadata); + } + + Version parseVersionMO2(QString const& value) + { + const auto match = s_SemVerMO2RegEx.match(value); + + if (!match.hasMatch()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid version string: '{}'", value))); + } + + const auto major = match.captured("major").toInt(); + const auto minor = match.captured("minor").toInt(); + const auto patch = match.captured("patch").toInt(); + + const auto subpatch = match.captured("subpatch").toInt(); + + // unlike semver, the regex will only match valid values + std::vector> prereleases; + if (match.hasCaptured("type")) { + prereleases.push_back(s_StringToRelease.at(match.captured("type"))); + + // for version with decimal point, e.g., 2.4.1rc1.1, we split the components into + // pre-release components to get {rc, 1, 1} - this works fine since {rc, 1} < {rc, + // 1, 1} + // + for (const auto& preVersion : + match.captured("prerelease").split(".", Qt::SkipEmptyParts)) { + prereleases.push_back(preVersion.toInt()); + } + } + + const auto buildMetadata = match.captured("buildmetadata").trimmed(); + + return Version(major, minor, patch, subpatch, prereleases, buildMetadata); + } + +} // namespace + +Version Version::parse(QString const& value, ParseMode mode) +{ + return mode == ParseMode::SemVer ? parseVersionSemVer(value) : parseVersionMO2(value); +} + +// constructors + +Version::Version(int major, int minor, int patch, QString metadata) + : Version(major, minor, patch, 0, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{}, m_BuildMetadata{std::move(metadata)} +{} + +Version::Version(int major, int minor, int patch, ReleaseType type, QString metadata) + : Version(major, minor, patch, 0, type, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, ReleaseType type, + QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{type}, m_BuildMetadata{std::move(metadata)} +{} + +Version::Version(int major, int minor, int patch, ReleaseType type, int prerelease, + QString metadata) + : Version(major, minor, patch, 0, type, prerelease, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, ReleaseType type, + int prerelease, QString metadata) + : Version(major, minor, patch, subpatch, {type, prerelease}, std::move(metadata)) +{} + +Version::Version(int major, int minor, int patch, int subpatch, + std::vector> prereleases, + QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{std::move(prereleases)}, m_BuildMetadata{std::move(metadata)} +{} + +// string + +QString Version::string(const FormatModes& modes) const +{ + const bool noSeparator = modes.testFlag(FormatMode::NoSeparator); + const bool shortAlphaBeta = modes.testFlag(FormatMode::ShortAlphaBeta); + auto value = std::format("{}.{}.{}", m_Major, m_Minor, m_Patch); + + if (m_SubPatch || modes.testFlag(FormatMode::ForceSubPatch)) { + value += std::format(".{}", m_SubPatch); + } + + if (!m_PreReleases.empty()) { + if (!noSeparator) { + value += "-"; + } + for (std::size_t i = 0; i < m_PreReleases.size(); ++i) { + value += std::visit( + [shortAlphaBeta](auto const& pre) -> std::string { + if constexpr (std::is_same_v) { + switch (pre) { + case Development: + return "dev"; + case Alpha: + return shortAlphaBeta ? "a" : "alpha"; + case Beta: + return shortAlphaBeta ? "b" : "beta"; + case ReleaseCandidate: + return "rc"; + } + return ""; + } else { + return std::to_string(pre); + } + }, + m_PreReleases[i]); + if (!noSeparator && i < m_PreReleases.size() - 1) { + value += "."; + } + } + } + + if (!modes.testFlag(FormatMode::NoMetadata) && !m_BuildMetadata.isEmpty()) { + value += "+" + m_BuildMetadata.toStdString(); + } + + return QString::fromStdString(value); +} + +namespace +{ + // consume the given iterator until the given end iterator or until a non-zero value + // is found + // + template + It consumePreReleaseZeros(It it, It end) + { + for (; it != end; ++it) { + if (!std::holds_alternative(*it) != 0 || std::get(*it) != 0) { + break; + } + } + return it; + }; +} // namespace + +std::strong_ordering operator<=>(const Version& lhs, const Version& rhs) +{ + auto mmp_cmp = + std::forward_as_tuple(lhs.major(), lhs.minor(), lhs.patch(), lhs.subpatch()) <=> + std::forward_as_tuple(rhs.major(), rhs.minor(), rhs.patch(), rhs.subpatch()); + + // major.minor.patch have precedence over everything else + if (mmp_cmp != std::strong_ordering::equal) { + return mmp_cmp; + } + + // handle cases were one is a pre-release and not the other - the pre-release is + // "less" than the release + if (lhs.isPreRelease() && !rhs.isPreRelease()) { + return std::strong_ordering::less; + } + + if (!lhs.isPreRelease() && rhs.isPreRelease()) { + return std::strong_ordering::greater; + } + + // compare pre-release fields + auto lhsIt = lhs.preReleases().begin(), rhsIt = rhs.preReleases().begin(); + for (; lhsIt != lhs.preReleases().end() && rhsIt != rhs.preReleases().end(); + ++lhsIt, ++rhsIt) { + + const auto &lhsPre = *lhsIt, rhsPre = *rhsIt; + + // if one is alpha/beta/etc. and the other is numeric, the alpha/beta/etc. is lower + // than the numeric one, which matches the index + auto pre_cmp = lhsPre.index() <=> rhsPre.index(); + if (pre_cmp != std::strong_ordering::equal) { + return pre_cmp; + } + + // compare the actual values + pre_cmp = lhsPre <=> rhsPre; + if (pre_cmp != std::strong_ordering::equal) { + return pre_cmp; + } + } + + // the code below does not follow semver 100% (I think) - basically, this makes stuff + // like 2.4.1rc1.0 equals to 2.4.1rc1, which according to semver is probably not right + // but is probably best for us + // + + // if we land here, we have consumed one of the pre-release, we skip all the 0 in the + // remaining one + lhsIt = consumePreReleaseZeros(lhsIt, lhs.preReleases().end()); + rhsIt = consumePreReleaseZeros(rhsIt, rhs.preReleases().end()); + + const auto lhsConsumed = lhsIt == lhs.preReleases().end(), + rhsConsumed = rhsIt == rhs.preReleases().end(); + + if (lhsConsumed && rhsConsumed) { + return std::strong_ordering::equal; + } else if (!lhsConsumed) { + return std::strong_ordering::greater; + } else { + return std::strong_ordering::less; + } +} + +} // namespace MOBase diff --git a/libs/uibase/src/widgetutility.cpp b/libs/uibase/src/widgetutility.cpp new file mode 100644 index 0000000..e3a4b06 --- /dev/null +++ b/libs/uibase/src/widgetutility.cpp @@ -0,0 +1,59 @@ +#include +#include +#include +#include +#include +#include + +namespace MOBase +{ + +void onHeaderContextMenu(QTreeView* view, const QPoint& pos) +{ + auto* header = view->header(); + + QMenu menu; + + // display a list of all headers as checkboxes + QAbstractItemModel* model = header->model(); + + for (int i = 1; i < model->columnCount(); ++i) { + const QString columnName = model->headerData(i, Qt::Horizontal).toString(); + + auto* checkBox = new QCheckBox(&menu); + checkBox->setText(columnName); + checkBox->setChecked(!header->isSectionHidden(i)); + + // Enable the checkbox if 1) the section is visible, or 2) the + // EnabledColumnRole is not found, or 3) the value for the role is true. + auto display = model->headerData(i, Qt::Horizontal, EnabledColumnRole); + checkBox->setEnabled(!header->isSectionHidden(i) || !display.isValid() || + display.toBool()); + + auto* checkableAction = new QWidgetAction(&menu); + checkableAction->setDefaultWidget(checkBox); + + QObject::connect(checkBox, &QCheckBox::clicked, [=] { + header->setSectionHidden(i, !checkBox->isChecked()); + }); + + menu.addAction(checkableAction); + } + + menu.exec(header->viewport()->mapToGlobal(pos)); +} + +void setCustomizableColumns(QTreeView* view) +{ + auto* header = view->header(); + + header->setSectionsMovable(true); + header->setContextMenuPolicy(Qt::CustomContextMenu); + + QObject::connect(header, &QWidget::customContextMenuRequested, view, + [view](auto&& pos) { + onHeaderContextMenu(view, pos); + }); +} + +} // namespace MOBase diff --git a/libs/uibase/tests/CMakeLists.txt b/libs/uibase/tests/CMakeLists.txt new file mode 100644 index 0000000..4483ff0 --- /dev/null +++ b/libs/uibase/tests/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +add_executable(uibase-tests EXCLUDE_FROM_ALL) +target_sources(uibase-tests + PRIVATE + test_main.cpp + test_formatters.cpp + test_ifiletree.cpp + test_strings.cpp + test_versioning.cpp +) +mo2_configure_tests(uibase-tests NO_SOURCES NO_MAIN NO_MOCK WARNINGS 4 AUTOMOC OFF) +target_link_libraries(uibase-tests PRIVATE uibase) diff --git a/libs/uibase/tests/cmake/CMakeLists.txt b/libs/uibase/tests/cmake/CMakeLists.txt new file mode 100644 index 0000000..e1d636f --- /dev/null +++ b/libs/uibase/tests/cmake/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.16) + +project(uibase-tests-cmake) + +find_package(mo2-uibase CONFIG REQUIRED) + +add_library(plugin SHARED) +target_sources(plugin PRIVATE plugin.cpp) +target_link_libraries(plugin PRIVATE mo2::uibase) +set_target_properties(plugin PROPERTIES CXX_STANDARD 20) diff --git a/libs/uibase/tests/cmake/plugin.cpp b/libs/uibase/tests/cmake/plugin.cpp new file mode 100644 index 0000000..8356dcd --- /dev/null +++ b/libs/uibase/tests/cmake/plugin.cpp @@ -0,0 +1,4 @@ +#include + +class Plugin : public MOBase::IPlugin +{}; diff --git a/libs/uibase/tests/test_formatters.cpp b/libs/uibase/tests/test_formatters.cpp new file mode 100644 index 0000000..4093112 --- /dev/null +++ b/libs/uibase/tests/test_formatters.cpp @@ -0,0 +1,48 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include +#pragma warning(pop) + +#include +#include + +#include + +#include + +TEST(FormatterTest, Enum) +{ + enum class E + { + a = 1, + b = 2 + }; + ASSERT_EQ("1", std::format("{}", E::a)); + ASSERT_EQ("2", std::format("{}", E::b)); +} + +// simply test that we can format between string types +TEST(FormatterTest, String) +{ + using namespace std::string_literals; + ASSERT_EQ("Hello World!", std::format("{}", L"Hello World!"s)); + ASSERT_EQ("Hello World!", std::format("{}", QString("Hello World!"))); + ASSERT_EQ(L"Hello World!", std::format(L"{}", QString("Hello World!"))); +} + +TEST(FormatterTest, RandomAccessContainer) +{ + // note: this is standard since C++23, so this does not test much, and unfortunately + // there is no way to customize the output format of the random access container + ASSERT_EQ("[]", std::format("{}", std::vector{})); + ASSERT_EQ("[1, 2, 3]", std::format("{}", std::vector{1, 2, 3})); + ASSERT_EQ("[1, 2, 3, 4, 5, 6, 7]", + std::format("{}", std::vector{1, 2, 3, 4, 5, 6, 7})); + + ASSERT_EQ("[\"AL\", \"Holt59\", \"Silarn\"]", + std::format("{}", QStringList{"AL", "Holt59", "Silarn"})); + + ASSERT_EQ("[QVariant(type=QString, value=MO2), QVariant(type=bool, value=true), " + "QVariant(type=int, value=45)]", + std::format("{}", QVariantList{"MO2", true, 45})); +} diff --git a/libs/uibase/tests/test_ifiletree.cpp b/libs/uibase/tests/test_ifiletree.cpp new file mode 100644 index 0000000..74fe3d7 --- /dev/null +++ b/libs/uibase/tests/test_ifiletree.cpp @@ -0,0 +1,1172 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include +#pragma warning(pop) + +#include +#include +#include +#include +#include + +#include + +std::ostream& operator<<(std::ostream& os, const QString& str) +{ + return os << str.toStdString(); +} + +using namespace MOBase; + +namespace std +{ +// If you can't declare the function in the class it's important that PrintTo() +// is defined in the SAME namespace that defines Point. C++'s look-up rules +// rely on that. +void PrintTo(std::shared_ptr entry, std::ostream* os) +{ + *os << entry->pathFrom(nullptr, "/"); +} +} // namespace std + +/** + * + */ +struct FileListTree : public IFileTree +{ + using File = std::pair; + + std::shared_ptr makeDirectory(std::shared_ptr parent, + QString name, + std::vector&& files) const + { + return std::shared_ptr( + new FileListTree(parent, name, std::move(files))); + } + + std::shared_ptr makeDirectory(std::shared_ptr parent, + QString name) const + { + return std::shared_ptr(new FileListTree(parent, name)); + } + + bool populated() const { return m_Populated; } + + virtual bool doPopulate(std::shared_ptr parent, + std::vector>& entries) const + { + // We know that the files are sorted: + QString currentName = ""; + std::vector currentFiles; + for (auto& p : m_Files) { + if (currentName == "") { + currentName = p.first[0]; + } + + if (currentName != p.first[0]) { + entries.push_back(makeDirectory(parent, currentName, std::move(currentFiles))); + currentFiles.clear(); + } + + currentName = p.first[0]; + + if (p.first.size() == 1) { + if (!p.second) { + entries.push_back(makeFile(parent, currentName)); + currentName = ""; + } + } else { + currentFiles.push_back( + {QStringList(p.first.begin() + 1, p.first.end()), p.second}); + } + } + + if (currentName != "") { + entries.push_back(makeDirectory(parent, currentName, std::move(currentFiles))); + } + + m_Populated = true; + + return false; + } + + virtual std::shared_ptr doClone() const override + { + return std::shared_ptr(new FileListTree(nullptr, name(), m_Files)); + } + +public: + static std::shared_ptr + makeTree(std::vector>&& files) + { + std::sort(std::begin(files), std::end(files), + [](const std::pair& a, const std::pair& b) { + return FileNameComparator::compare(a.first, b.first) < 0; + }); + + std::vector pFiles; + + for (auto p : files) { + pFiles.push_back({p.first.split("/", Qt::SkipEmptyParts), p.second}); + } + + return std::shared_ptr( + new FileListTree(nullptr, "", std::move(pFiles))); + } + +protected: + FileListTree(std::shared_ptr parent, QString name) + : FileTreeEntry(parent, name), IFileTree() + {} + FileListTree(std::shared_ptr parent, QString name, + std::vector const& files) + : FileTreeEntry(parent, name), IFileTree(), m_Files(files) + {} + FileListTree(std::shared_ptr parent, QString name, + std::vector&& files) + : FileTreeEntry(parent, name), IFileTree(), m_Files(std::move(files)) + {} + + mutable bool m_Populated = false; + std::vector m_Files; +}; + +/** + * @brief Check if the given tree has been populated. + * + * Since IFileTree does not expose the "populated" flag, this is a convenient + * method that simply downcast to `FileListTree` and check `populated()` on it. + * + * @param tree The tree to check. + * + * @return true if the tree has been populated, false otherwize. + */ +bool populated(std::shared_ptr tree) +{ + return std::dynamic_pointer_cast(tree)->populated(); +} + +/** + * @brief Retrieve all the entry in the given tree. + * + * @param fileTree The tree to get the entries from. + * + * @return a vector containing all the entries in the tree. + */ +std::vector> +getAllEntries(std::shared_ptr fileTree) +{ + std::vector> entries; + for (auto entry : *fileTree) { + entries.push_back(entry); + if (entry->isDir()) { + auto childEntries = getAllEntries(entry->astree()); + entries.insert(entries.end(), childEntries.begin(), childEntries.end()); + } + } + return entries; +} + +/** + * @brief Check that the given file tree match the given entries. + * + * This is probably pretty slow but it is only for unit testing. This will check + * both way: all entries in the vector must be in the tree at the right place, and + * all entries in the tree must be in the vector. + * + * @param fileTree The tree to check. + * @param entries The entries to check. Filenames must be separated by /. Must contain + * all the entry, including intermediate directories, except the root. + * + */ +void assertTreeEquals(std::shared_ptr fileTree, + std::vector> const& entries) +{ + // Check that all entries are in the tree: + for (auto& entry : entries) { + auto treeEntry = fileTree->find(entry.first); + ASSERT_NE(treeEntry, nullptr) + << "Entry " << entry.first << " not found in the tree."; + ASSERT_EQ(entry.second, treeEntry->isDir()) + << "Entry " << entry.first << " is not of the right type."; + } + + // Check that all entries in the tree are in the vector: + auto treeEntries = getAllEntries(fileTree); + for (auto& entry : treeEntries) { + auto path = entry->pathFrom(fileTree, "/"); + auto it = std::find_if(entries.begin(), entries.end(), [&path](auto const& p) { + return p.first.compare(path, Qt::CaseInsensitive) == 0; + }); + ASSERT_NE(it, entries.end()) << "Entry '" << path << "' not expected in the tree."; + ASSERT_EQ(it->second, entry->isDir()) + << "Entry '" << path << "' is not of the right type."; + } +} + +/** + * @brief Create a mapping from path to file entry for the given tree. + * + * @param fileTree The tree to create the mapping from. + * + * @return a mapping from path (separated by /) to file entry. + */ +std::map> +createMapping(std::shared_ptr fileTree) +{ + std::map> mapping; + for (auto entry : *fileTree) { + mapping[entry->path("/")] = entry; + if (entry->isDir()) { + auto tmp = createMapping(entry->astree()); + mapping.insert(std::begin(tmp), std::end(tmp)); + } + } + return mapping; +} + +TEST(IFileTreeTest, ExtensionComputedCorrectly) +{ + // Fake tree to create entry: + std::shared_ptr fileTree = FileListTree::makeTree({}); + + auto a = fileTree->addFile("a.txt"); + EXPECT_EQ(a->name(), "a.txt"); + EXPECT_EQ(a->suffix(), "txt"); + + fileTree->move(a, "a.c.b"); + EXPECT_EQ(a->name(), "a.c.b"); + EXPECT_EQ(a->suffix(), "b"); +} + +TEST(IFileTreeTest, TreeIsPopulatedCorrectly) +{ + std::vector> strTree{{"a/", true}, {"b", true}, + {"c.x", false}, {"d.y", false}, + {"e/q/c.t", false}, {"e/q/p", true}}; + + std::shared_ptr fileTree = FileListTree::makeTree(std::move(strTree)); + + ASSERT_NE(fileTree, nullptr); + + ASSERT_TRUE(fileTree->exists("a")); + ASSERT_TRUE(fileTree->exists("b")); + ASSERT_TRUE(fileTree->exists("c.x")); + ASSERT_TRUE(fileTree->exists("d.y")); + ASSERT_TRUE(fileTree->exists("e")); + ASSERT_TRUE(fileTree->exists("e/q")); + ASSERT_TRUE(fileTree->exists("e/q/c.t")); + ASSERT_TRUE(fileTree->exists("e/q/p")); + + assertTreeEquals(fileTree, {{"a", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e", true}, + {"e/q", true}, + {"e/q/c.t", false}, + {"e/q/p", true}}); + + // Retrieve the entry: + { + std::shared_ptr a = fileTree->find("a"), b = fileTree->find("b"), + cx = fileTree->find("c.x"), + dy = fileTree->find("d.y"), e = fileTree->find("e"), + e_q = fileTree->find("e/q"), + e_q_ct = fileTree->find("e/q/c.t"), + e_q_p = fileTree->find("e/q/p"); + + EXPECT_NE(a, nullptr); + EXPECT_TRUE(a->isDir()); + EXPECT_EQ(a->astree(), a); + EXPECT_EQ(a->name(), "a"); + EXPECT_EQ(a->path("/"), "a"); + EXPECT_NE(b, nullptr); + EXPECT_TRUE(b->isDir()); + EXPECT_EQ(b->astree(), b); + EXPECT_EQ(b->name(), "b"); + EXPECT_EQ(b->path("/"), "b"); + EXPECT_NE(cx, nullptr); + EXPECT_TRUE(cx->isFile()); + EXPECT_EQ(cx->astree(), nullptr); + EXPECT_EQ(cx->name(), "c.x"); + EXPECT_EQ(cx->path("/"), "c.x"); + EXPECT_NE(dy, nullptr); + EXPECT_TRUE(dy->isFile()); + EXPECT_EQ(dy->astree(), nullptr); + EXPECT_EQ(dy->name(), "d.y"); + EXPECT_EQ(dy->path("/"), "d.y"); + EXPECT_NE(e, nullptr); + EXPECT_TRUE(e->isDir()); + EXPECT_EQ(e->astree(), e); + EXPECT_EQ(e->name(), "e"); + EXPECT_EQ(e->path("/"), "e"); + EXPECT_NE(e_q, nullptr); + EXPECT_TRUE(e_q->isDir()); + EXPECT_EQ(e_q->astree(), e_q); + EXPECT_EQ(e_q->name(), "q"); + EXPECT_EQ(e_q->path("/"), "e/q"); + EXPECT_NE(e_q_ct, nullptr); + EXPECT_TRUE(e_q_ct->isFile()); + EXPECT_EQ(e_q_ct->astree(), nullptr); + EXPECT_EQ(e_q_ct->name(), "c.t"); + EXPECT_EQ(e_q_ct->path("/"), "e/q/c.t"); + EXPECT_NE(e_q_p, nullptr); + EXPECT_TRUE(e_q_p->isDir()); + EXPECT_EQ(e_q_p->astree(), e_q_p); + EXPECT_EQ(e_q_p->name(), "p"); + EXPECT_EQ(e_q_p->path("/"), "e/q/p"); + + // Some relation check: + EXPECT_EQ(a->parent(), fileTree); + EXPECT_EQ(b->parent(), fileTree); + EXPECT_EQ(cx->parent(), fileTree); + EXPECT_EQ(dy->parent(), fileTree); + EXPECT_EQ(e->parent(), fileTree); + EXPECT_EQ(e_q->parent(), e->astree()); + EXPECT_EQ(e_q_ct->parent(), e_q->astree()); + EXPECT_EQ(e_q_p->parent(), e_q->astree()); + + // Check that we can reach the children: + EXPECT_EQ(e->astree()->find("q"), e_q); + EXPECT_EQ(e->astree()->find("q/c.t"), e_q_ct); + EXPECT_EQ(e->astree()->find("q/p"), e_q_p); + + // Check the content: + EXPECT_EQ(a->astree()->size(), std::size_t{0}); + EXPECT_TRUE(a->astree()->empty()); + EXPECT_EQ(a->astree()->begin(), a->astree()->end()); + EXPECT_EQ(b->astree()->size(), std::size_t{0}); + EXPECT_TRUE(b->astree()->empty()); + EXPECT_EQ(b->astree()->begin(), b->astree()->end()); + EXPECT_EQ(cx->astree(), nullptr); + EXPECT_EQ(dy->astree(), nullptr); + EXPECT_EQ(e->astree()->size(), std::size_t{1}); + EXPECT_EQ(e->astree()->at(0), e_q); + EXPECT_EQ(e_q->astree()->size(), std::size_t{2}); + EXPECT_NE(std::find(e_q->astree()->begin(), e_q->astree()->end(), e_q_ct), + e_q->astree()->end()); + EXPECT_NE(std::find(e_q->astree()->begin(), e_q->astree()->end(), e_q_p), + e_q->astree()->end()); + + EXPECT_EQ(a->pathFrom(fileTree), "a"); + EXPECT_EQ(a->path(), "a"); + EXPECT_EQ(b->pathFrom(fileTree), "b"); + EXPECT_EQ(b->path(), "b"); + EXPECT_EQ(cx->path(), "c.x"); + EXPECT_EQ(dy->path(), "d.y"); + EXPECT_EQ(e->path(), "e"); + EXPECT_EQ(e_q->path(), "e\\q"); + EXPECT_EQ(e_q->pathFrom(e->astree()), "q"); + EXPECT_EQ(e_q_ct->path("/"), "e/q/c.t"); + EXPECT_EQ(e_q_ct->pathFrom(e->astree()), "q\\c.t"); + EXPECT_EQ(e_q_ct->pathFrom(e_q->astree(), "/"), "c.t"); + EXPECT_EQ(e_q_p->path(), "e\\q\\p"); + EXPECT_EQ(e_q_p->path("/"), "e/q/p"); + EXPECT_EQ(e_q_p->pathFrom(e->astree()), "q\\p"); + EXPECT_EQ(e_q_p->pathFrom(e_q->astree()), "p"); + + EXPECT_EQ(a->pathFrom(b->astree()), ""); + EXPECT_EQ(b->pathFrom(a->astree()), ""); + EXPECT_EQ(e->pathFrom(e_q->astree()), ""); + } + + { + std::shared_ptr a = fileTree->find("a", FileTreeEntry::DIRECTORY), + b = fileTree->find("b", FileTreeEntry::DIRECTORY), + cx = fileTree->find("c.x", FileTreeEntry::FILE), + dy = fileTree->find("d.y", FileTreeEntry::FILE), + e = fileTree->find("e", FileTreeEntry::DIRECTORY), + e_q = + fileTree->find("e/q", FileTreeEntry::DIRECTORY), + e_q_ct = + fileTree->find("e/q/c.t", FileTreeEntry::FILE), + e_q_p = fileTree->find("e/q/p", + FileTreeEntry::DIRECTORY); + + EXPECT_TRUE((a != nullptr && a->isDir() && a->name() == "a")); + EXPECT_TRUE((b != nullptr && b->isDir() && b->name() == "b")); + EXPECT_TRUE((cx != nullptr && cx->isFile() && cx->name() == "c.x")); + EXPECT_TRUE((dy != nullptr && dy->isFile() && dy->name() == "d.y")); + EXPECT_TRUE((e != nullptr && e->isDir() && e->name() == "e")); + EXPECT_TRUE((e_q != nullptr && e_q->isDir() && e_q->name() == "q")); + EXPECT_TRUE((e_q_ct != nullptr && e_q_ct->isFile() && e_q_ct->name() == "c.t")); + EXPECT_TRUE((e_q_p != nullptr && e_q_p->isDir() && e_q_p->name() == "p")); + + EXPECT_EQ(fileTree->find("a", FileTreeEntry::FILE), nullptr); + EXPECT_EQ(fileTree->find("b", FileTreeEntry::FILE), nullptr); + EXPECT_EQ(fileTree->find("c.x", FileTreeEntry::DIRECTORY), nullptr); + EXPECT_EQ(fileTree->find("d.y", FileTreeEntry::DIRECTORY), nullptr); + EXPECT_EQ(fileTree->find("e", FileTreeEntry::FILE), nullptr); + EXPECT_EQ(fileTree->find("e/q", FileTreeEntry::FILE), nullptr); + EXPECT_EQ(fileTree->find("e/q/c.t", FileTreeEntry::DIRECTORY), nullptr); + EXPECT_EQ(fileTree->find("e/q/p", FileTreeEntry::FILE), nullptr); + } +} + +TEST(IFileTreeTest, TreeIsDestructedCorrectly) +{ + std::vector> strTree{{"a/", true}, {"b", true}, + {"c.x", false}, {"d.y", false}, + {"e/q/c.t", false}, {"e/q/p", true}}; + + std::shared_ptr fileTree = FileListTree::makeTree(std::move(strTree)); + + EXPECT_NE(fileTree, nullptr); + + // Retrieve weak ptr for the entry: + std::weak_ptr a = fileTree->find("a"), b = fileTree->find("b"), + cx = fileTree->find("c.x"), dy = fileTree->find("d.y"), + e = fileTree->find("e"), e_q = fileTree->find("e/q"), + e_q_ct = fileTree->find("e/q/c.t"), + e_q_p = fileTree->find("e/q/p"); + + // And for the trees: + std::weak_ptr r_t = fileTree, a_t = a.lock()->astree(), + b_t = b.lock()->astree(), e_t = e.lock()->astree(), + e_q_t = e_q.lock()->astree(), + e_q_p_t = e_q_p.lock()->astree(); + + // Release the base tree: + fileTree.reset(); + + EXPECT_TRUE(a.expired()); + EXPECT_TRUE(b.expired()); + EXPECT_TRUE(cx.expired()); + EXPECT_TRUE(dy.expired()); + EXPECT_TRUE(e.expired()); + EXPECT_TRUE(e_q.expired()); + EXPECT_TRUE(e_q_ct.expired()); + EXPECT_TRUE(e_q_p.expired()); + + EXPECT_TRUE(a_t.expired()); + EXPECT_TRUE(b_t.expired()); + EXPECT_TRUE(e_t.expired()); + EXPECT_TRUE(e_q_t.expired()); + EXPECT_TRUE(e_q_p_t.expired()); +} + +TEST(IFileTreeTest, BasicTreeManipulation) +{ + std::vector> strTree{{"a/", true}, {"b", true}, + {"c.x", false}, {"d.y", false}, + {"e/q/c.t", false}, {"e/q/p", true}}; + + std::shared_ptr fileTree = FileListTree::makeTree(std::move(strTree)); + + EXPECT_NE(fileTree, nullptr); + + // Retrieve the entry: + std::shared_ptr a = fileTree->find("a"), b = fileTree->find("b"), + cx = fileTree->find("c.x"), dy = fileTree->find("d.y"), + e = fileTree->find("e"), e_q = fileTree->find("e/q"), + e_q_ct = fileTree->find("e/q/c.t"), + e_q_p = fileTree->find("e/q/p"); + + EXPECT_TRUE(b->moveTo(a->astree())); + EXPECT_FALSE(fileTree->exists("b")); + EXPECT_EQ(fileTree->find("a/b"), b); + EXPECT_TRUE(a->astree()->exists("b")); + EXPECT_EQ(a->astree()->find("b"), b); + EXPECT_EQ(a->astree()->size(), std::size_t{1}); + EXPECT_EQ(a->astree()->at(0), b); +} + +TEST(IFileTreeTest, IterOperations) +{ + auto tree = + FileListTree::makeTree({{"a", true}, {"c", true}, {"b", false}, {"d", false}}); + + // Order should be a -> c -> b -> d + std::vector expected{tree->find("a"), tree->find("c"), tree->find("b"), + tree->find("d")}; + std::vector entries(std::begin(*tree), std::end(*tree)); + EXPECT_EQ(entries, expected); + + // Order should be reversed: + expected = std::vector(expected.rbegin(), expected.rend()); + entries = std::vector(std::rbegin(*tree), std::rend(*tree)); + EXPECT_EQ(entries, expected); + + // We can erasae in the middle: + for (auto it = tree->begin(); it != tree->end();) { + if ((*it)->name() == "b") { + it = tree->erase(*it); + // Check that the returned iterator is valid (it should be the iterator + // to d): + EXPECT_EQ(it, tree->end() - 1); + EXPECT_EQ(*it, tree->find("d")); + } else { + ++it; + } + } + assertTreeEquals(tree, {{"a", true}, {"c", true}, {"d", false}}); +} + +TEST(IFileTreeTest, AddOperations) +{ + { + auto fileTree = FileListTree::makeTree( + {{"a", true}, {"c.x", false}, {"e/q/c.t", false}, {"e/q/p", true}}); + auto map = createMapping(fileTree); + + EXPECT_EQ(fileTree->addFile("a"), nullptr); + EXPECT_EQ(fileTree->addFile("c.x"), nullptr); + EXPECT_EQ(fileTree->addFile("e"), nullptr); + EXPECT_EQ(fileTree->addFile("e/q"), nullptr); + EXPECT_EQ(fileTree->addFile("e/q/c.t"), nullptr); + EXPECT_EQ(fileTree->addFile("e/q/p"), nullptr); + + auto a_p = fileTree->addFile("a/p"); + EXPECT_NE(a_p, nullptr); + EXPECT_EQ(a_p->parent(), map["a"]); + + auto e_q_ct = fileTree->addFile("e/q/c.t", true); + EXPECT_NE(e_q_ct, nullptr); + EXPECT_EQ(e_q_ct->parent(), map["e/q"]); + EXPECT_EQ(map["e/q/c.t"]->parent(), nullptr); + EXPECT_EQ(map["e/q"]->astree()->size(), std::size_t{2}); + + // Directory are replaced with addFile(): + auto e_q = fileTree->addFile("e/q", true); + EXPECT_NE(e_q, nullptr); + EXPECT_EQ(e_q->parent(), map["e"]); + EXPECT_EQ(map["e/q"]->parent(), nullptr); + EXPECT_EQ(map["e"]->astree()->size(), std::size_t{1}); + } +} + +TEST(IFileTreeTest, TreeInsertOperations) +{ + + // Test failure: + { + auto fileTree = FileListTree::makeTree({{"a/", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/p", true}, + {"e/q/z/", true}, + {"e/q/z/a.t", false}, + {"e/q/z/b", true}, + {"f/q/c.t", false}, + {"f/q/o", true}, + {"f/q/z/b", false}, + {"f/q/z/c.t", false}}); + + EXPECT_NE(fileTree, nullptr); + + // Retrieve the entry: + auto map = createMapping(fileTree); + auto e = fileTree->findDirectory("e"); + auto f_q = fileTree->findDirectory("f/q"); + + auto it = e->insert(f_q, IFileTree::InsertPolicy::FAIL_IF_EXISTS); + EXPECT_EQ(it, e->end()); + EXPECT_EQ(f_q->parent(), fileTree->find("f")); + } + + // Test replace: + { + auto fileTree = FileListTree::makeTree({{"a/", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/p", true}, + {"e/q/z/", true}, + {"e/q/z/a.t", false}, + {"e/q/z/b", true}, + {"f/q/c.t", false}, + {"f/q/o", true}, + {"f/q/z/b", false}, + {"f/q/z/c.t", false}}); + + EXPECT_NE(fileTree, nullptr); + + // Retrieve the entry: + auto map = createMapping(fileTree); + auto e = fileTree->findDirectory("e"); + auto f_q = fileTree->findDirectory("f/q"); + + auto it = e->insert(f_q, IFileTree::InsertPolicy::REPLACE); + EXPECT_NE(it, e->end()); + EXPECT_EQ(f_q->parent(), e); + EXPECT_EQ(map["e/q"]->parent(), nullptr); + EXPECT_EQ(e->find("q"), map["f/q"]); + EXPECT_TRUE(fileTree->findDirectory("f")->empty()); + EXPECT_EQ(e->find("q/c.t"), map["f/q/c.t"]); + EXPECT_EQ(e->find("q/o"), map["f/q/o"]); + EXPECT_EQ(e->find("q/z"), map["f/q/z"]); + EXPECT_EQ(e->find("q/z/b"), map["f/q/z/b"]); + EXPECT_EQ(e->find("q/z/c.t"), map["f/q/z/c.t"]); + } + + // Test merge: + { + auto fileTree = FileListTree::makeTree({{"a/", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/p", true}, + {"e/q/z", true}, + {"e/q/z/a.t", false}, + {"e/q/z/b", true}, + {"f/q/c.t", false}, + {"f/q/o", true}, + {"f/q/z", true}, + {"f/q/z/b", false}, + {"f/q/z/c.t", false}}); + + EXPECT_NE(fileTree, nullptr); + + // Retrieve the entry: + auto map = createMapping(fileTree); + auto e = fileTree->findDirectory("e"); + auto f_q = fileTree->findDirectory("f/q"); + + auto it = e->insert(f_q, IFileTree::InsertPolicy::MERGE); + assertTreeEquals(e, {{"q", true}, + {"q/o", true}, + {"q/p", true}, + {"q/z", true}, + {"q/c.t", false}, + {"q/z/a.t", false}, + {"q/z/c.t", false}, + {"q/z/b", false}}); + EXPECT_EQ(e->find("q/z/b"), map["f/q/z/b"]); + EXPECT_EQ(fileTree->findDirectory("f")->size(), std::size_t{0}); + EXPECT_EQ(map["f/q"]->parent(), nullptr); + EXPECT_EQ(map["f/q/z"]->parent(), nullptr); + } +} + +TEST(IFileTreeTest, TreeMoveAndCopyOperations) +{ + { + auto tree1 = FileListTree::makeTree( + {{"a/b/m.y", false}, {"a/b/c", true}, {"b/", true}, {"c", false}}); + auto a = tree1->findDirectory("a"); + EXPECT_FALSE(populated(a)); + + tree1->move(tree1->find("a"), "a1"); + + // Moving the tree should not have populated it: + EXPECT_EQ(tree1->find("a"), nullptr); + EXPECT_EQ(tree1->find("a1"), a); + EXPECT_FALSE(populated(a)); + + tree1->copy(tree1->find("a1"), "a2"); + + // Copying the tree should not have populated it: + EXPECT_FALSE(populated(a)); + EXPECT_FALSE(populated(tree1->findDirectory("a2"))); + EXPECT_EQ(tree1->find("a1"), a); + EXPECT_NE(tree1->find("a1"), tree1->find("a2")); + + assertTreeEquals(tree1, { + {"a1", true}, + {"a1/b", true}, + {"a1/b/c", true}, + {"a1/b/m.y", false}, + {"a2", true}, + {"a2/b", true}, + {"a2/b/c", true}, + {"a2/b/m.y", false}, + {"b", true}, + {"c", false}, + }); + + // Everything should be populated now: + EXPECT_TRUE(populated(tree1->findDirectory("a1"))); + EXPECT_TRUE(populated(tree1->findDirectory("a2"))); + + QString a1("a1/"), a2("a2/"); + for (auto p : {"b", "b/c", "b/m.y"}) { + EXPECT_NE(tree1->find(a1 + p), tree1->find(a2 + p)) + << "Entry '" << (a1 + p) << "' and '" << (a2 + p) << "' should be different."; + } + } +} + +TEST(IFileTreeTest, TreeMergeOperations) +{ + + { + auto fileTree = FileListTree::makeTree({{"a/", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/p", true}}); + + EXPECT_NE(fileTree, nullptr); + + // Retrieve the entry: + auto map = createMapping(fileTree); + auto e = fileTree->findDirectory("e"); + auto e_q = fileTree->findDirectory("e/q"); + + // Merge e in the root: + IFileTree::OverwritesType overwrites; + auto noverwrites = fileTree->merge(e, &overwrites); + + EXPECT_EQ(noverwrites, std::size_t{0}); + EXPECT_TRUE(overwrites.empty()); + EXPECT_EQ(e->size(), std::size_t{0}); + assertTreeEquals(fileTree, {{"a", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e", true}, + {"q", true}, + {"q/c.t", false}, + {"q/p", true}}); + + auto p = fileTree->addFile("p"); + EXPECT_NE(p, nullptr); + + // Not: e/q is not q + overwrites.clear(); + noverwrites = fileTree->merge(e_q, &overwrites); + EXPECT_EQ(noverwrites, std::size_t{1}); + EXPECT_EQ(overwrites.size(), std::size_t{1}); + EXPECT_EQ(overwrites[p], map["e/q/p"]); + assertTreeEquals(fileTree, {{"a", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e", true}, + {"q", true}, + {"c.t", false}, + {"p", true}}); + // Note: the "p" at the root should be the one under q initially. + EXPECT_EQ(fileTree->find("p"), map["e/q/p"]); + } + + // Merge failure: + { + auto tree1 = FileListTree::makeTree({{"a/", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/p", true}}); + + std::size_t noverwrites = tree1->findDirectory("e")->merge(tree1); + EXPECT_EQ(noverwrites, IFileTree::MERGE_FAILED); + + noverwrites = tree1->findDirectory("e/q")->merge(tree1); + EXPECT_EQ(noverwrites, IFileTree::MERGE_FAILED); + + noverwrites = tree1->merge(tree1); + EXPECT_EQ(noverwrites, IFileTree::MERGE_FAILED); + } + + // + { + auto tree1 = FileListTree::makeTree({{"a/b/c/m.y", false}, + {"a/b/c/n", true}, + {"a/b/x.t", false}, + {"a/b/y.t", false}, + {"b/", true}, + {"c", false}}); + auto map1 = createMapping(tree1); + + auto tree2 = FileListTree::makeTree({{"a/b/c/m.y", false}, + {"a/b/c/n", false}, // n is a file here + {"a/b/y.t", false}, + {"b/v", false}, + {"b/e", true}}); + auto map2 = createMapping(tree2); + + IFileTree::OverwritesType overwrites; + std::size_t noverwrites = tree1->merge(tree2, &overwrites); + + EXPECT_EQ(noverwrites, std::size_t{3}); + EXPECT_EQ(noverwrites, overwrites.size()); + EXPECT_EQ(overwrites[map1["a/b/c/m.y"]], map2["a/b/c/m.y"]); + EXPECT_EQ(overwrites[map1["a/b/c/n"]], map2["a/b/c/n"]); + EXPECT_EQ(overwrites[map1["a/b/y.t"]], map2["a/b/y.t"]); + + assertTreeEquals(tree1, {{"a", true}, + {"b", true}, + {"c", false}, + {"a/b", true}, + {"a/b/c", true}, + {"a/b/c/m.y", false}, + {"a/b/c/n", false}, + {"a/b/x.t", false}, + {"a/b/y.t", false}, + {"b/v", false}, + {"b/e", true}}); + + // Merged directories should be the one from the original tree: + EXPECT_EQ(tree1->find("a"), map1["a"]); + EXPECT_EQ(tree1->find("a/b"), map1["a/b"]); + EXPECT_EQ(tree1->find("a/b/c"), map1["a/b/c"]); + EXPECT_EQ(tree1->find("b"), map1["b"]); + + // Overriden: + EXPECT_EQ(tree1->find("a/b/c/m.y"), map2["a/b/c/m.y"]); + EXPECT_EQ(tree1->find("a/b/c/n"), map2["a/b/c/n"]); + EXPECT_EQ(tree1->find("a/b/y.t"), map2["a/b/y.t"]); + } +} + +TEST(IFileTreeTest, TreeWalkOperations) +{ + + auto fileTree = FileListTree::makeTree({{"a/", true}, + {"b", true}, + {"b/u", false}, + {"b/v", false}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/p", true}}); + + auto map = createMapping(fileTree); + + // Note: Testing specific order here, while in reality user should not rely + // on it (and it is not specified, on purpose). Only guarantee is that a folder + // is visited before its children. + { + // Populate the vector: + std::vector>> entries; + fileTree->walk( + [&entries](auto path, auto entry) { + entries.push_back({path, entry}); + return IFileTree::WalkReturn::CONTINUE; + }, + "/"); + + decltype(entries) expected{{"", map["a"]}, {"", map["b"]}, + {"b/", map["b/u"]}, {"b/", map["b/v"]}, + {"", map["e"]}, {"e/", map["e/q"]}, + {"e/q/", map["e/q/p"]}, {"e/q/", map["e/q/c.t"]}, + {"", map["c.x"]}, {"", map["d.y"]}}; + EXPECT_EQ(entries, expected); + + entries.clear(); + fileTree->walk( + [&entries](auto path, auto entry) { + if (entry->name() == "e") { + return IFileTree::WalkReturn::STOP; + } + entries.push_back({path, entry}); + return IFileTree::WalkReturn::CONTINUE; + }, + "/"); + + // Note: This assumes a given order, while in reality it is not specified. + expected = { + {"", map["a"]}, + {"", map["b"]}, + {"b/", map["b/u"]}, + {"b/", map["b/v"]}, + }; + EXPECT_EQ(entries, expected); + + entries.clear(); + fileTree->walk( + [&entries](auto path, auto entry) { + if (entry->name() == "e") { + return IFileTree::WalkReturn::SKIP; + } + entries.push_back({path, entry}); + return IFileTree::WalkReturn::CONTINUE; + }, + "/"); + + // Note: This assumes a given order, while in reality it is not specified. + expected = {{"", map["a"]}, {"", map["b"]}, {"b/", map["b/u"]}, + {"b/", map["b/v"]}, {"", map["c.x"]}, {"", map["d.y"]}}; + EXPECT_EQ(entries, expected); + } + + // same as above but with generator version + { + // Populate the vector: + auto entries = walk(fileTree) | std::ranges::to(); + decltype(entries) expected{map["a"], map["b"], map["b/u"], map["b/v"], + map["e"], map["e/q"], map["e/q/p"], map["e/q/c.t"], + map["c.x"], map["d.y"]}; + EXPECT_EQ(entries, expected); + + entries.clear(); + for (const auto entry : walk(fileTree)) { + if (entry->name() == "e") { + break; // Stop on e + } + entries.push_back(entry); + } + + // Note: This assumes a given order, while in reality it is not specified. + expected = { + map["a"], + map["b"], + map["b/u"], + map["b/v"], + }; + EXPECT_EQ(entries, expected); + + // note: third test with SKIP is not possible with generator version + } +} + +TEST(IFileTreeTest, TreeGlobOperations) +{ + using entrySet = std::unordered_set>; + + const auto REGEX = GlobPatternType::REGEX; + + { + auto fileTree = FileListTree::makeTree({{"a/", true}, + {"a/g.t", false}, + {"b", true}, + {"b/u", false}, + {"b/v", false}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/m.x", false}, + {"e/q/p", true}}); + + auto map = createMapping(fileTree); + + entrySet entries, expected; + + entries = glob(fileTree, "*") | std::ranges::to(); + expected = {map["a"], map["b"], map["c.x"], map["d.y"], map["e"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, ".*", REGEX) | std::ranges::to(); + expected = {map["a"], map["b"], map["c.x"], map["d.y"], map["e"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**") | std::ranges::to(); + expected = {fileTree, map["a"], map["b"], map["e"], map["e/q"], map["e/q/p"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**", REGEX) | std::ranges::to(); + expected = {fileTree, map["a"], map["b"], map["e"], map["e/q"], map["e/q/p"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "*.x") | std::ranges::to(); + expected = {map["c.x"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, ".*[.]x", REGEX) | std::ranges::to(); + expected = {map["c.x"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/*.x") | std::ranges::to(); + expected = {map["c.x"], map["e/q/m.x"]}; + EXPECT_EQ(entries, expected); + + entries = + glob(fileTree, "**/.*[.]x", REGEX) | std::ranges::to(); + expected = {map["c.x"], map["e/q/m.x"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "*.t") | std::ranges::to(); + expected = {}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/*.t") | std::ranges::to(); + expected = {map["a/g.t"], map["e/q/c.t"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "a/*") | std::ranges::to(); + expected = {map["a/g.t"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "a/.*", REGEX) | std::ranges::to(); + expected = {map["a/g.t"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/*.[xt]") | std::ranges::to(); + expected = {map["c.x"], map["e/q/m.x"], map["a/g.t"], map["e/q/c.t"]}; + EXPECT_EQ(entries, expected); + + entries = + glob(fileTree, "**/.*[.][xt]", REGEX) | std::ranges::to(); + expected = {map["c.x"], map["e/q/m.x"], map["a/g.t"], map["e/q/c.t"]}; + EXPECT_EQ(entries, expected); + } + + { + auto fileTree = FileListTree::makeTree({{"aq.js", false}, + {"bb", true}, + {"cm.tx", false}, + {"dp.js", false}, + {"ev", false}, + {"go.ya", false}, + {"gw.md", false}, + {"hh", false}, + {"hl", true}, + {"in", true}, + {"mz", true}, + {"sc", true}, + {"bb/ce.cp", false}, + {"bb/cm.tx", false}, + {"bb/gw", true}, + {"bb/iw.cp", false}, + {"bb/js", true}, + {"bb/px.cp", false}, + {"hl/ds.in", false}, + {"in/nu", true}, + {"mz/tu.js", false}, + {"sc/cm.tx", false}, + {"sc/cw.ts", false}, + {"sc/cz.rc", false}, + {"sc/dr.cp", false}, + {"sc/hh.cp", false}, + {"sc/kn.ui", false}, + {"sc/lr.cp", false}, + {"sc/nd.o", false}, + {"sc/nv.o", false}, + {"sc/rv.ui", false}, + {"sc/tv.h", false}, + {"bb/gw/cp.qm", false}, + {"bb/gw/hq.qm", false}, + {"bb/gw/pu.ts", false}, + {"bb/gw/tu.ts", false}, + {"bb/js/cm.tx", false}, + {"bb/js/co.cp", false}, + {"in/nu/el.h", false}, + {"in/nu/fj.h", false}, + {"in/nu/lw", true}, + {"in/nu/xx", true}, + {"in/nu/lw/cp.h", false}, + {"in/nu/lw/go.h", false}, + {"in/nu/xx/ap.h", false}, + {"in/nu/xx/qz.h", false}}); + + auto map = createMapping(fileTree); + + entrySet entries, expected; + + entries = glob(fileTree, "*.h") | std::ranges::to(); + expected = {}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "*") | std::ranges::to(); + expected = {map.at("aq.js"), map.at("bb"), map.at("cm.tx"), map.at("dp.js"), + map.at("ev"), map.at("go.ya"), map.at("gw.md"), map.at("hh"), + map.at("hl"), map.at("in"), map.at("mz"), map.at("sc")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "*/*") | std::ranges::to(); + expected = { + map.at("bb/ce.cp"), map.at("bb/cm.tx"), map.at("bb/gw"), map.at("bb/iw.cp"), + map.at("bb/js"), map.at("bb/px.cp"), map.at("hl/ds.in"), map.at("in/nu"), + map.at("mz/tu.js"), map.at("sc/cm.tx"), map.at("sc/cw.ts"), map.at("sc/cz.rc"), + map.at("sc/dr.cp"), map.at("sc/hh.cp"), map.at("sc/kn.ui"), map.at("sc/lr.cp"), + map.at("sc/nd.o"), map.at("sc/nv.o"), map.at("sc/rv.ui"), map.at("sc/tv.h")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "*/*/*") | std::ranges::to(); + expected = {map.at("bb/gw/cp.qm"), map.at("bb/gw/hq.qm"), map.at("bb/gw/pu.ts"), + map.at("bb/gw/tu.ts"), map.at("bb/js/cm.tx"), map.at("bb/js/co.cp"), + map.at("in/nu/el.h"), map.at("in/nu/fj.h"), map.at("in/nu/lw"), + map.at("in/nu/xx")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**") | std::ranges::to(); + expected = {fileTree, map.at("bb"), map.at("bb/gw"), map.at("bb/js"), + map.at("hl"), map.at("in"), map.at("in/nu"), map.at("in/nu/lw"), + map.at("in/nu/xx"), map.at("mz"), map.at("sc")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/*") | std::ranges::to(); + expected = {map.at("aq.js"), + map.at("bb"), + map.at("cm.tx"), + map.at("dp.js"), + map.at("ev"), + map.at("go.ya"), + map.at("gw.md"), + map.at("hh"), + map.at("hl"), + map.at("in"), + map.at("mz"), + map.at("sc"), + map.at("bb/ce.cp"), + map.at("bb/cm.tx"), + map.at("bb/gw"), + map.at("bb/iw.cp"), + map.at("bb/js"), + map.at("bb/px.cp"), + map.at("bb/gw/cp.qm"), + map.at("bb/gw/hq.qm"), + map.at("bb/gw/pu.ts"), + map.at("bb/gw/tu.ts"), + map.at("bb/js/cm.tx"), + map.at("bb/js/co.cp"), + map.at("hl/ds.in"), + map.at("in/nu"), + map.at("in/nu/el.h"), + map.at("in/nu/fj.h"), + map.at("in/nu/lw"), + map.at("in/nu/xx"), + map.at("in/nu/lw/cp.h"), + map.at("in/nu/lw/go.h"), + map.at("in/nu/xx/ap.h"), + map.at("in/nu/xx/qz.h"), + map.at("mz/tu.js"), + map.at("sc/cm.tx"), + map.at("sc/cw.ts"), + map.at("sc/cz.rc"), + map.at("sc/dr.cp"), + map.at("sc/hh.cp"), + map.at("sc/kn.ui"), + map.at("sc/lr.cp"), + map.at("sc/nd.o"), + map.at("sc/nv.o"), + map.at("sc/rv.ui"), + map.at("sc/tv.h")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/cm.tx") | std::ranges::to(); + expected = {map.at("cm.tx"), map.at("bb/cm.tx"), map.at("bb/js/cm.tx"), + map.at("sc/cm.tx")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/sc/**/cm.tx") | std::ranges::to(); + expected = {map.at("sc/cm.tx")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/sc") | std::ranges::to(); + expected = {map.at("sc")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "in/**") | std::ranges::to(); + expected = {map.at("in"), map.at("in/nu"), map.at("in/nu/lw"), map.at("in/nu/xx")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "in/**/**") | std::ranges::to(); + expected = {map.at("in"), map.at("in/nu"), map.at("in/nu/lw"), map.at("in/nu/xx")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "in/*/*") | std::ranges::to(); + expected = {map.at("in/nu/el.h"), map.at("in/nu/fj.h"), map.at("in/nu/lw"), + map.at("in/nu/xx")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "in/*/*.h") | std::ranges::to(); + expected = {map.at("in/nu/el.h"), map.at("in/nu/fj.h")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "sc/**/*.cp") | std::ranges::to(); + expected = {map.at("sc/dr.cp"), map.at("sc/hh.cp"), map.at("sc/lr.cp")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "sc/**/n*.o") | std::ranges::to(); + expected = {map.at("sc/nd.o"), map.at("sc/nv.o")}; + EXPECT_EQ(entries, expected); + } +} diff --git a/libs/uibase/tests/test_main.cpp b/libs/uibase/tests/test_main.cpp new file mode 100644 index 0000000..0399b43 --- /dev/null +++ b/libs/uibase/tests/test_main.cpp @@ -0,0 +1,15 @@ +#include + +#include +#include + +int main(int argc, char** argv) +{ + QCoreApplication app(argc, argv); + QTranslator translator; + if (translator.load("tests_fr", "tests/translations")) { + app.installTranslator(&translator); + } + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/libs/uibase/tests/test_strings.cpp b/libs/uibase/tests/test_strings.cpp new file mode 100644 index 0000000..0185f83 --- /dev/null +++ b/libs/uibase/tests/test_strings.cpp @@ -0,0 +1,47 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include +#pragma warning(pop) + +#include + +#include + +#include + +using namespace MOBase; + +TEST(StringsTest, IEquals) +{ + ASSERT_TRUE(iequals("hello world", "HelLO WOrlD")); +} + +TEST(StringsTest, IReplaceAll) +{ + auto ireplace_all = [](std::string_view input, std::string_view search, + std::string_view replace) { + std::string s_input{input}; + MOBase::ireplace_all(s_input, search, replace); + return s_input; + }; + + ASSERT_EQ("", ireplace_all("", "world", "MO2")); + ASSERT_EQ("Hello World!", ireplace_all("Hello World!", "Test", "MO2")); + ASSERT_EQ("replace a stuff with a stuff a", + ireplace_all("replace some stuff with some stuff some", "some", "a")); + ASSERT_EQ("replace a stuff with a stuff som", + ireplace_all("replace some stuff with some stuff som", "some", "a")); + ASSERT_EQ("1YYY3YYY2", ireplace_all("1aBc3AbC2", "abC", "YYY")); + + ASSERT_EQ( + "data path: C:/Users/USERNAME/AppData/Local/ModOrganizer/Starfield", + ireplace_all("data path: C:/Users/lords/AppData/Local/ModOrganizer/Starfield", + "/lords", "/USERNAME")); +} + +// this is more a tests of the tests +TEST(StringsTest, Translation) +{ + ASSERT_EQ("Traduction en Français", + QCoreApplication::translate("uibase-tests", "Translate to French")); +} diff --git a/libs/uibase/tests/test_versioning.cpp b/libs/uibase/tests/test_versioning.cpp new file mode 100644 index 0000000..08a346f --- /dev/null +++ b/libs/uibase/tests/test_versioning.cpp @@ -0,0 +1,90 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include +#pragma warning(pop) + +#include +#include + +#include + +#include + +using namespace MOBase; + +using enum Version::ReleaseType; +using ParseMode = Version::ParseMode; + +TEST(VersioningTest, VersionParse) +{ + // TODO: add exceptions test + + // semver + ASSERT_EQ(Version(1, 0, 0), Version::parse("1.0.0")); + ASSERT_EQ(Version(1, 0, 0, Development, 1), Version::parse("1.0.0-dev.1")); + ASSERT_EQ(Version(1, 0, 0, Development, 2), Version::parse("1.0.0-dev.2")); + ASSERT_EQ(Version(1, 0, 0, Alpha), Version::parse("1.0.0-a")); + ASSERT_EQ(Version(1, 0, 0, Alpha), Version::parse("1.0.0-alpha")); + ASSERT_EQ(Version(1, 0, 0, 0, {Alpha, 1, Beta}), Version::parse("1.0.0-alpha.1.b")); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0-beta.2")); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 1), Version::parse("2.5.2-rc.1")); + + // mo2 + ASSERT_EQ(Version(1, 0, 0), Version::parse("1.0.0", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Development, 1), + Version::parse("1.0.0dev1", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Development, 2), + Version::parse("1.0.0dev2", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Alpha, 1), Version::parse("1.0.0a1", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Alpha, 1), Version::parse("1.0.0alpha1", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0beta2", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0beta2", ParseMode::MO2)); + ASSERT_EQ(Version(2, 4, 1, 0, {ReleaseCandidate, 1, 1}), + Version::parse("2.4.1rc1.1", ParseMode::MO2)); + ASSERT_EQ(Version(2, 2, 2, 1, Beta, 2), + Version::parse("2.2.2.1beta2", ParseMode::MO2)); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 1), + Version::parse("v2.5.2rc1", ParseMode::MO2)); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 2), + Version::parse("2.5.2rc2", ParseMode::MO2)); +} + +TEST(VersioningTest, VersionString) +{ + ASSERT_EQ("1.0.0", Version(1, 0, 0).string()); + ASSERT_EQ("1.0.0-dev.1", Version(1, 0, 0, Development, 1).string()); + ASSERT_EQ("1.0.0-dev.2", Version(1, 0, 0, Development, 2).string()); + ASSERT_EQ("1.0.0-alpha", Version(1, 0, 0, Alpha).string()); + ASSERT_EQ("1.0.0-alpha.1.beta", Version(1, 0, 0, 0, {Alpha, 1, Beta}).string()); + ASSERT_EQ("1.0.0-beta.2", Version(1, 0, 0, Beta, 2).string()); + ASSERT_EQ("2.5.2-rc.1", Version(2, 5, 2, ReleaseCandidate, 1).string()); + ASSERT_EQ("2.5.2rc1", + Version(2, 5, 2, ReleaseCandidate, 1).string(Version::FormatCondensed)); +} + +TEST(VersioningTest, VersionCompare) +{ + // shortcut + using v = Version; + + // test from https://semver.org/ + ASSERT_TRUE(v(1, 0, 0) < v(2, 0, 0)); + ASSERT_TRUE(v(2, 0, 0) < v(2, 1, 0)); + ASSERT_TRUE(v(2, 1, 0) < v(2, 1, 1)); + + ASSERT_TRUE(v(1, 0, 0, Alpha) < v(1, 0, 0, Alpha, 1)); + ASSERT_TRUE(v(1, 0, 0, Alpha, 1) < v(1, 0, 0, 0, {Alpha, Beta})); + ASSERT_TRUE(v(1, 0, 0, 0, {Alpha, Beta}) < v(1, 0, 0, 1)); + ASSERT_TRUE(v(1, 0, 0, Beta) < v(1, 0, 0, Beta, 2)); + ASSERT_TRUE(v(1, 0, 0, Beta, 2) < v(1, 0, 0, Beta, 11)); + ASSERT_TRUE(v(1, 0, 0, Beta, 11) < v(1, 0, 0, ReleaseCandidate, 1)); + ASSERT_TRUE(v(1, 0, 0, ReleaseCandidate, 0) < v(1, 0, 0)); + + ASSERT_TRUE(v(2, 4, 1, 0, {ReleaseCandidate, 1, 0}) == + v(2, 4, 1, ReleaseCandidate, 1)); + ASSERT_TRUE(v(2, 4, 1, 0, {ReleaseCandidate, 1, 0}) < + v(2, 4, 1, 0, {ReleaseCandidate, 1, 1})); + ASSERT_TRUE(v(2, 4, 1, ReleaseCandidate, 1) < + v(2, 4, 1, 0, {ReleaseCandidate, 1, 1})); + ASSERT_TRUE(v(1, 0, 0) < v(2, 0, 0, Alpha)); +} diff --git a/libs/uibase/tests/translations/tests_en.qm b/libs/uibase/tests/translations/tests_en.qm new file mode 100644 index 0000000..d5ca501 Binary files /dev/null and b/libs/uibase/tests/translations/tests_en.qm differ diff --git a/libs/uibase/tests/translations/tests_en.ts b/libs/uibase/tests/translations/tests_en.ts new file mode 100644 index 0000000..1f54d86 --- /dev/null +++ b/libs/uibase/tests/translations/tests_en.ts @@ -0,0 +1,11 @@ + + + + + uibase-tests + + Translate to French + Translate to French + + + diff --git a/libs/uibase/tests/translations/tests_fr.qm b/libs/uibase/tests/translations/tests_fr.qm new file mode 100644 index 0000000..80958d6 Binary files /dev/null and b/libs/uibase/tests/translations/tests_fr.qm differ diff --git a/libs/uibase/tests/translations/tests_fr.ts b/libs/uibase/tests/translations/tests_fr.ts new file mode 100644 index 0000000..eed02a0 --- /dev/null +++ b/libs/uibase/tests/translations/tests_fr.ts @@ -0,0 +1,11 @@ + + + + + uibase-tests + + Translate to French + Traduction en Français + + + diff --git a/libs/uibase/vcpkg.json b/libs/uibase/vcpkg.json new file mode 100644 index 0000000..4777918 --- /dev/null +++ b/libs/uibase/vcpkg.json @@ -0,0 +1,34 @@ +{ + "dependencies": ["spdlog"], + "overrides": [ + { + "name": "spdlog", + "version": "1.15.3" + } + ], + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake"] + }, + "testing": { + "description": "Build UI Base tests.", + "dependencies": ["gtest"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673", + "packages": ["mo2-cmake", "spdlog"] + } + ] + } +} diff --git a/libs/usvfs/.clang-format b/libs/usvfs/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/usvfs/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/usvfs/.git-blame-ignore-revs b/libs/usvfs/.git-blame-ignore-revs new file mode 100644 index 0000000..f5b2c23 --- /dev/null +++ b/libs/usvfs/.git-blame-ignore-revs @@ -0,0 +1 @@ +1c95b7452585e53ef97954248aed42480d5bb5de diff --git a/libs/usvfs/.gitattributes b/libs/usvfs/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/usvfs/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/usvfs/.github/workflows/build.yml b/libs/usvfs/.github/workflows/build.yml new file mode 100644 index 0000000..2acd535 --- /dev/null +++ b/libs/usvfs/.github/workflows/build.yml @@ -0,0 +1,173 @@ +name: Build USVFS + +on: + push: + branches: [master, dev/cmake] + tags: + - "*" + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + +jobs: + build: + name: Build USVFS + strategy: + matrix: + arch: [x86, x64] + config: [Debug, Release] + runs-on: windows-2022 + steps: + # set VCPKG Root + - name: "Set environmental variables" + shell: bash + run: | + echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> $GITHUB_ENV + + # checkout USVFS and vcpkg + - uses: actions/checkout@v4 + + # configure + - run: cmake --preset vs2022-windows-${{ matrix.arch }} -B build_${{ matrix.arch }} "-DCMAKE_INSTALL_PREFIX=install/${{ matrix.config }}" + + # build + - run: cmake --build build_${{ matrix.arch }} --config ${{ matrix.config }} --target INSTALL + + # package install + - uses: actions/upload-artifact@master + with: + name: usvfs_${{ matrix.config }}_${{ matrix.arch }} + path: ./install/${{ matrix.config }} + + # package test/dlls/etc. for tests + + - uses: actions/upload-artifact@master + with: + name: usvfs-libs_${{ matrix.config }}_${{ matrix.arch }} + path: ./lib + - uses: actions/upload-artifact@master + with: + name: usvfs-bins_${{ matrix.config }}_${{ matrix.arch }} + path: ./bin + - uses: actions/upload-artifact@master + with: + name: usvfs-tests_${{ matrix.config }}_${{ matrix.arch }} + path: ./test/bin + + # merge x86 / x64 artifacts for tests (root bin/lib and test folder) + merge-artifacts-for-tests: + runs-on: ubuntu-latest + name: Merge Test Artifacts + needs: build + strategy: + matrix: + config: [Debug, Release] + steps: + - name: Merge USVFS libs + uses: actions/upload-artifact/merge@v4 + with: + name: usvfs-libs_${{ matrix.config }} + pattern: usvfs-libs_${{ matrix.config }}_* + - name: Merge USVFS bins + uses: actions/upload-artifact/merge@v4 + with: + name: usvfs-bins_${{ matrix.config }} + pattern: usvfs-bins_${{ matrix.config }}_* + - name: Merge USVFS tests + uses: actions/upload-artifact/merge@v4 + with: + name: usvfs-tests_${{ matrix.config }} + pattern: usvfs-tests_${{ matrix.config }}_* + + # merge x86 / x64 artifacts (install folder) + merge-artifacts-for-release: + runs-on: ubuntu-latest + name: Merge Install Artifacts + needs: build + strategy: + matrix: + config: [Debug, Release] + steps: + - name: Merge USVFS install + uses: actions/upload-artifact/merge@v4 + with: + name: usvfs_${{ matrix.config }} + pattern: usvfs_${{ matrix.config }}_* + + test: + name: Test USVFS + needs: merge-artifacts-for-tests + runs-on: windows-2022 + strategy: + matrix: + config: [Debug, Release] + arch: [x86, x64] + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@master + with: + name: usvfs-libs_${{ matrix.config }} + path: ./lib + - uses: actions/download-artifact@master + with: + name: usvfs-bins_${{ matrix.config }} + path: ./bin + - uses: actions/download-artifact@master + with: + name: usvfs-tests_${{ matrix.config }} + path: ./test/bin + - run: ./test/bin/shared_test_${{ matrix.arch }}.exe + if: always() + - run: ./test/bin/testinject_bin_${{ matrix.arch }}.exe + if: always() + - run: ./test/bin/thooklib_test_${{ matrix.arch }}.exe + if: always() + - run: ./test/bin/tinjectlib_test_${{ matrix.arch }}.exe + if: always() + - run: ./test/bin/tvfs_test_${{ matrix.arch }}.exe + if: always() + - run: ./test/bin/usvfs_test_runner_${{ matrix.arch }}.exe + if: always() + - run: ./test/bin/usvfs_global_test_runner_${{ matrix.arch }}.exe + if: always() + - uses: actions/upload-artifact@v4 + if: always() + with: + name: tests-outputs_${{ matrix.config }}_${{ matrix.arch }} + path: ./test/temp + if-no-files-found: ignore + retention-days: 7 + overwrite: true + + publish: + if: github.ref_type == 'tag' + needs: [merge-artifacts-for-release, test] + runs-on: windows-2022 + permissions: + contents: write + steps: + # USVFS does not use different names for debug and release so we are going to + # retrieve both install artifacts and put them under install/ and install/debug/ + + - name: Download Release Artifact + uses: actions/download-artifact@master + with: + name: usvfs_Release + path: ./install + + - name: Download Debug Artifact + uses: actions/download-artifact@master + with: + name: usvfs_Debug + path: ./install/debug + + - name: Create USVFS Base archive + run: 7z a usvfs_${{ github.ref_name }}.7z ./install/* + + - name: Publish Release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh release create --draft=false --notes="Release ${{ github.ref_name }}" "${{ github.ref_name }}" ./usvfs_${{ github.ref_name }}.7z diff --git a/libs/usvfs/.github/workflows/linting.yml b/libs/usvfs/.github/workflows/linting.yml new file mode 100644 index 0000000..d5e3f4b --- /dev/null +++ b/libs/usvfs/.github/workflows/linting.yml @@ -0,0 +1,17 @@ +name: Lint USVFS + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." + exclude-regex: "third-party" diff --git a/libs/usvfs/.gitignore b/libs/usvfs/.gitignore new file mode 100644 index 0000000..1c1dec2 --- /dev/null +++ b/libs/usvfs/.gitignore @@ -0,0 +1,34 @@ +# build outputs: +/bin +/lib +/test/bin +/build* +/install + +# local overides +/vsbuild/external_dependencies_local.props + +# build intermediates: +/vsbuild/Release +/vsbuild/ReleaseTest +/vsbuild/Debug +/vsbuild/DebugTest +/vsbuild32 +/vsbuild64 +CMakeUserPresets.json + +# test "side effects" +/test/temp + +# VS generated files: +.vs +*.aps +*.vcxproj.user +msbuild.log + +/stderr.log +/stderr_32.log +/stdout.log +/stdout_32.log + +.vscode diff --git a/libs/usvfs/.pre-commit-config.yaml b/libs/usvfs/.pre-commit-config.yaml new file mode 100644 index 0000000..3103a1f --- /dev/null +++ b/libs/usvfs/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v19.1.5 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/usvfs/CMakeLists.txt b/libs/usvfs/CMakeLists.txt new file mode 100644 index 0000000..e0deb42 --- /dev/null +++ b/libs/usvfs/CMakeLists.txt @@ -0,0 +1,75 @@ +cmake_minimum_required(VERSION 3.16) + +include(CMakePackageConfigHelpers) + +project(usvfs) + +set_property(GLOBAL PROPERTY USE_FOLDERS ON) +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +set(CMAKE_CXX_STANDARD 20) + +set(USVFS_BINDIR ${CMAKE_CURRENT_LIST_DIR}/bin) +set(USVFS_LIBDIR ${CMAKE_CURRENT_LIST_DIR}/lib) + +if (MSVC) + # /Zi generate PDBs + # /Gy enable function-level linking + # /Oi enable intrinsic function + add_compile_options("$<$>:/Zi;/Gy;/Oi>") + + # /OPT:ICF enable COMDAT folding + # /DEBUG:FULL generate debug info (PDB) + # /OPT:REF enable references (PDB) + add_link_options("$<$>:/OPT:ICF;/DEBUG:FULL;/OPT:REF>") +endif() + +if(CMAKE_SIZEOF_VOID_P EQUAL 4) + set(ARCH_POSTFIX _x86) +else() + set(ARCH_POSTFIX _x64) +endif() + +add_subdirectory(src/shared) + +add_subdirectory(src/thooklib) +add_subdirectory(src/tinjectlib) +add_subdirectory(src/usvfs_helper) + +add_subdirectory(src/usvfs_dll) +add_subdirectory(src/usvfs_proxy) + +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/usvfsConfig.cmake" + INSTALL_DESTINATION "lib/cmake/usvfs" + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO +) + +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/include/usvfs/usvfs_version.h" usvfs_version) +string(REGEX MATCH "USVFS_VERSION_MAJOR ([0-9]*)" _ ${usvfs_version}) +set(usvfs_version_major ${CMAKE_MATCH_1}) +string(REGEX MATCH "USVFS_VERSION_MINOR ([0-9]*)" _ ${usvfs_version}) +set(usvfs_version_minor ${CMAKE_MATCH_1}) +string(REGEX MATCH "USVFS_VERSION_BUILD ([0-9]*)" _ ${usvfs_version}) +set(usvfs_version_build ${CMAKE_MATCH_1}) +string(REGEX MATCH "USVFS_VERSION_REVISION ([0-9]*)" _ ${usvfs_version}) +set(usvfs_version_revision ${CMAKE_MATCH_1}) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/usvfsConfigVersion.cmake" + VERSION "${usvfs_version_major}.${usvfs_version_minor}.${usvfs_version_build}.${usvfs_version_revision}" + COMPATIBILITY AnyNewerVersion + ARCH_INDEPENDENT +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/usvfsConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/usvfsConfigVersion.cmake + DESTINATION lib/cmake/usvfs +) + +if (BUILD_TESTING) + enable_testing() + set(USVFS_TEST_BINDIR ${CMAKE_CURRENT_LIST_DIR}/test/bin) + add_subdirectory(test) +endif() diff --git a/libs/usvfs/CMakePresets.json b/libs/usvfs/CMakePresets.json new file mode 100644 index 0000000..156ccfe --- /dev/null +++ b/libs/usvfs/CMakePresets.json @@ -0,0 +1,102 @@ +{ + "configurePresets": [ + { + "cacheVariables": { + "BUILD_TESTING": { + "type": "BOOL", + "value": "ON" + } + }, + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "testing" + } + }, + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild64", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "hidden": true, + "name": "windows-x64" + }, + { + "binaryDir": "${sourceDir}/vsbuild32", + "architecture": { + "strategy": "set", + "value": "Win32" + }, + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x86-windows-static-md" + } + }, + "hidden": true, + "name": "windows-x86" + }, + { + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4" + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "hidden": true, + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "inherits": ["vs2022-windows", "windows-x64"], + "name": "vs2022-windows-x64" + }, + { + "inherits": ["vs2022-windows", "windows-x86"], + "name": "vs2022-windows-x86" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows-x64", + "configurePreset": "vs2022-windows-x64" + }, + { + "name": "vs2022-windows-x86", + "configurePreset": "vs2022-windows-x86" + } + ], + "version": 3 +} diff --git a/libs/usvfs/LICENSE b/libs/usvfs/LICENSE new file mode 100644 index 0000000..c0000a0 --- /dev/null +++ b/libs/usvfs/LICENSE @@ -0,0 +1,699 @@ +Copyright (C) 2015-2024 Sebastian Herbord, ModOrganizer2 Team + +This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +Please find a copy of the of the GNU General Public License at the end of this file or see . + +Additional permissions are granted under GNU GPL version 3 section 7 to Free and Open Source Software ("FOSS") without requiring that such software is covered by the GPLv3. + + 1. Permission to link with the usvfs_x64.dll and/or usvfs_x86.dll + + 2. Permission to distribute unmodified binary copies of the usvfs library + +Theses permissions (and no other) are granted provided that the software: + + 1. Is distributed under a license that satisfies the Free Software Definition (https://www.gnu.org/philosophy/free-sw.en.html) or the Open Source Definition Version (https://opensource.org/osd) + + 2. Includes the copyright notice "usvfs - User-Space Virtual File System, Copyright (C) Sebastian Herbord", a copy of this license and a link to the usvfs repository in its user-interface and any user-facing documentation. + + 3. Is not linked or distributed with proprietary (non-FOSS) software. + + +------------------------------------------------------------------------- + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/libs/usvfs/README.md b/libs/usvfs/README.md new file mode 100644 index 0000000..1167810 --- /dev/null +++ b/libs/usvfs/README.md @@ -0,0 +1,59 @@ +# USVFS + +[![License](https://img.shields.io/:license-gpl-blue.svg)](http://www.gnu.org/licenses/gpl-3.0.en.html) +[![Build](https://github.com/ModOrganizer2/usvfs/actions/workflows/build.yml/badge.svg)](https://github.com/ModOrganizer2/usvfs/actions) + +USVFS (short for User Space Virtual File System) aims to allow windows applications to +create file or directory links that are visible to only a select set of processes. +It does so by using api hooking to fool file access functions into discovering/opening +files that are in fact somewhere else. + +## Current state + +USVFS is work in progress and should be considered in alpha state. +It is a core component of Mod Organizer v2 +and thus receives serious real world testing. + +## Building + +You will need `cmake`, Python 3+ and `vcpkg` to build USVFS: + +```pwsh +cmake --preset vs2022-windows-x64 +cmake --build --preset vs2022-windows-x64 --config Release + +# only if you need to hook x86 applications +cmake --preset vs2022-windows-x86 +cmake --build --preset vs2022-windows-x86 --config Release +``` + +## Comparison to symbolic links + +The following is based on the final goal for USVFS and doesn't necessary reflect the +current development state. + +Unlike symbolic file links provided by NTFS + +- links aren't visible to all applications but only to those the caller chooses +- links disappear when the "session ends" +- doesn't require write access to the link destination +- doesn't require administrator rights (neither for installation nor for use) +- links are filesystem independent so you can create links on fat32 drives, read-only media and network drives +- can link multiple directories on top of a single destination (overlaying) +- can also "virtually" unlink files, thus make them invisible to processes or replace existing files + +There are of course drawbacks + +- will always impose a memory and cpu overhead though hopefully those will be marginal +- becomes active only during the initialization phase of each process so it may not be active at the time dependent dlls are loaded +- introduces a new source of bugs that can cause hard to diagnose problems in affected processes +- may rub antivirus software the wrong way as the used techniques are similar to what some malware does. + +## License + +USVFS is currently licensed under the GPLv3 but this may change in the future. + +## Contributing + +Contributions are very welcome but please notice that since I'm still undecided on +licensing I have to ask all contributors to agree to future licensing changes. diff --git a/libs/usvfs/cmake/config.cmake.in b/libs/usvfs/cmake/config.cmake.in new file mode 100644 index 0000000..b5e85ee --- /dev/null +++ b/libs/usvfs/cmake/config.cmake.in @@ -0,0 +1,6 @@ +@PACKAGE_INIT@ + +include ( "${CMAKE_CURRENT_LIST_DIR}/usvfs_x86Targets.cmake" ) +include ( "${CMAKE_CURRENT_LIST_DIR}/usvfs_x64Targets.cmake" ) + +add_library(usvfs::usvfs ALIAS usvfs_x64::usvfs_dll) diff --git a/libs/usvfs/include/usvfs/dllimport.h b/libs/usvfs/include/usvfs/dllimport.h new file mode 100644 index 0000000..c17f648 --- /dev/null +++ b/libs/usvfs/include/usvfs/dllimport.h @@ -0,0 +1,31 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#ifndef DLLEXPORT + +#ifdef BUILDING_USVFS_DLL +#define DLLEXPORT __declspec(dllexport) +#else +#define DLLEXPORT __declspec(dllimport) +#endif + +#endif DLLEXPORT diff --git a/libs/usvfs/include/usvfs/logging.h b/libs/usvfs/include/usvfs/logging.h new file mode 100644 index 0000000..4647df5 --- /dev/null +++ b/libs/usvfs/include/usvfs/logging.h @@ -0,0 +1,29 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +enum class LogLevel : uint8_t +{ + Debug, + Info, + Warning, + Error +}; diff --git a/libs/usvfs/include/usvfs/sharedparameters.h b/libs/usvfs/include/usvfs/sharedparameters.h new file mode 100644 index 0000000..f428dc0 --- /dev/null +++ b/libs/usvfs/include/usvfs/sharedparameters.h @@ -0,0 +1,112 @@ +#pragma once + +#include "dllimport.h" +#include "usvfsparameters.h" +#include + +namespace usvfs +{ + +class ForcedLibrary +{ +public: + ForcedLibrary(const std::string& processName, const std::string& libraryPath, + const shared::VoidAllocatorT& allocator); + + std::string processName() const; + std::string libraryPath() const; + +private: + shared::StringT m_processName; + shared::StringT m_libraryPath; +}; + +class DLLEXPORT SharedParameters +{ +public: + SharedParameters() = delete; + SharedParameters(const SharedParameters& reference) = delete; + SharedParameters& operator=(const SharedParameters& reference) = delete; + + SharedParameters(const usvfsParameters& reference, + const shared::VoidAllocatorT& allocator); + + usvfsParameters makeLocal() const; + + std::string instanceName() const; + std::string currentSHMName() const; + std::string currentInverseSHMName() const; + void setSHMNames(const std::string& current, const std::string& inverse); + + void setDebugParameters(LogLevel level, CrashDumpsType dumpType, + const std::string& dumpPath, + std::chrono::milliseconds delayProcess); + + std::size_t userConnected(); + std::size_t userDisconnected(); + std::size_t userCount(); + + std::size_t registeredProcessCount() const; + std::vector registeredProcesses() const; + void registerProcess(DWORD pid); + void unregisterProcess(DWORD pid); + + void blacklistExecutable(const std::string& name); + void clearExecutableBlacklist(); + bool executableBlacklisted(const std::string& app, const std::string& cmd) const; + + void addSkipFileSuffix(const std::string& fileSuffix); + void clearSkipFileSuffixes(); + std::vector skipFileSuffixes() const; + + void addSkipDirectory(const std::string& directory); + void clearSkipDirectories(); + std::vector skipDirectories() const; + + void addForcedLibrary(const std::string& process, const std::string& path); + std::vector forcedLibraries(const std::string& processName); + void clearForcedLibraries(); + +private: + using StringAllocatorT = shared::VoidAllocatorT::rebind::other; + + using DWORDAllocatorT = shared::VoidAllocatorT::rebind::other; + + using ForcedLibraryAllocatorT = shared::VoidAllocatorT::rebind::other; + + using ProcessBlacklist = + boost::container::flat_set, + StringAllocatorT>; + + using ProcessList = + boost::container::flat_set, DWORDAllocatorT>; + + using FileSuffixSkipList = + boost::container::flat_set, + StringAllocatorT>; + + using DirectorySkipList = + boost::container::flat_set, + StringAllocatorT>; + + using ForcedLibraries = + boost::container::slist; + + mutable bi::interprocess_mutex m_mutex; + shared::StringT m_instanceName; + shared::StringT m_currentSHMName; + shared::StringT m_currentInverseSHMName; + bool m_debugMode; + LogLevel m_logLevel; + CrashDumpsType m_crashDumpsType; + shared::StringT m_crashDumpsPath; + std::chrono::milliseconds m_delayProcess; + uint32_t m_userCount; + ProcessBlacklist m_processBlacklist; + ProcessList m_processList; + FileSuffixSkipList m_fileSuffixSkipList; + DirectorySkipList m_directorySkipList; + ForcedLibraries m_forcedLibraries; +}; + +} // namespace usvfs diff --git a/libs/usvfs/include/usvfs/usvfs.h b/libs/usvfs/include/usvfs/usvfs.h new file mode 100644 index 0000000..6754922 --- /dev/null +++ b/libs/usvfs/include/usvfs/usvfs.h @@ -0,0 +1,232 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "dllimport.h" +#include "usvfsparameters.h" + +/* + * Virtual operations: + * - link file + * - link directory (empty) + * - link directory (static) + * - link directory (dynamic) + * - delete file + * - delete directory + * Maybe: + * - rename/move (= copy + delete) + * - copy-on-write semantics (changes to files are done in a separate copy of the + * file, the original is kept on disc but hidden) + */ + +static const unsigned int LINKFLAG_FAILIFEXISTS = + 0x00000001; // if set, linking fails in case of an error +static const unsigned int LINKFLAG_MONITORCHANGES = + 0x00000002; // if set, changes to the source directory after the link operation + // will be updated in the virtual fs. only relevant in static + // link directory operations +static const unsigned int LINKFLAG_CREATETARGET = + 0x00000004; // if set, file creation (including move or copy) operations to + // destination will be redirected to the source. Only one createtarget + // can be set for a destination folder so this flag will replace + // the previous create target. + // If there different create-target have been set for an element and + // one of its ancestors, the inner-most create-target is used +static const unsigned int LINKFLAG_RECURSIVE = + 0x00000008; // if set, directories are linked recursively +static const unsigned int LINKFLAG_FAILIFSKIPPED = + 0x00000010; // if set, linking fails if the file or directory is skipped + // files or directories are skipped depending on whats been added to + // the skip file suffixes or skip directories list in + // the sharedparameters class, those lists are checked during virtual + // linking + +extern "C" +{ + + /** + * removes all virtual mappings + */ + DLLEXPORT void WINAPI usvfsClearVirtualMappings(); + + /** + * link a file virtually + * @note: the directory the destination file resides in has to exist - at least + * virtually. + */ + DLLEXPORT BOOL WINAPI usvfsVirtualLinkFile(LPCWSTR source, LPCWSTR destination, + unsigned int flags); + + /** + * link a directory virtually. This static variant recursively links all files + * individually, change notifications are used to update the information. + * @param failIfExists if true, this call fails if the destination directory exists + * (virtually or physically) + */ + DLLEXPORT BOOL WINAPI usvfsVirtualLinkDirectoryStatic(LPCWSTR source, + LPCWSTR destination, + unsigned int flags); + + /** + * connect to a virtual filesystem as a controller, without hooking the calling + * process. Please note that you can only be connected to one vfs, so this will + * silently disconnect from a previous vfs. + */ + DLLEXPORT BOOL WINAPI usvfsConnectVFS(const usvfsParameters* p); + + /** + * @brief create a new VFS. This is similar to ConnectVFS except it guarantees + * the vfs is reset before use. + */ + DLLEXPORT BOOL WINAPI usvfsCreateVFS(const usvfsParameters* p); + + /** + * disconnect from a virtual filesystem. This removes hooks if necessary + */ + DLLEXPORT void WINAPI usvfsDisconnectVFS(); + + DLLEXPORT void WINAPI usvfsGetCurrentVFSName(char* buffer, size_t size); + + /** + * retrieve a list of all processes connected to the vfs + */ + DLLEXPORT BOOL WINAPI usvfsGetVFSProcessList(size_t* count, LPDWORD processIDs); + + // retrieve a list of all processes connected to the vfs, stores an array + // of `count` elements in `*buffer` + // + // if this returns TRUE and `count` is not 0, the caller must release the buffer + // with `free(*buffer)` + // + // return values: + // - ERROR_INVALID_PARAMETERS: either `count` or `buffer` is NULL + // - ERROR_TOO_MANY_OPEN_FILES: there seems to be way too many usvfs processes + // running, probably some internal error + // - ERROR_NOT_ENOUGH_MEMORY: malloc() failed + // + DLLEXPORT BOOL WINAPI usvfsGetVFSProcessList2(size_t* count, DWORD** buffer); + + /** + * spawn a new process that can see the virtual file system. The signature is + * identical to CreateProcess + */ + DLLEXPORT BOOL WINAPI usvfsCreateProcessHooked( + LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, + LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, + DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation); + + /** + * retrieve a single log message. + * FIXME There is currently no way to unblock from the caller side + * FIXME retrieves log messages from all instances, the logging queue is not separated + */ + DLLEXPORT bool WINAPI usvfsGetLogMessages(LPSTR buffer, size_t size, + bool blocking = false); + + /** + * retrieves a readable representation of the vfs tree + * @param buffer the buffer to write to. this may be null if you only want to + * determine the required buffer size + * @param size pointer to a variable that contains the buffer. After the call + * this value will have been updated to contain the required size, + * even if this is bigger than the buffer size + */ + DLLEXPORT BOOL WINAPI usvfsCreateVFSDump(LPSTR buffer, size_t* size); + + /** + * adds an executable to the blacklist so it doesn't get exposed to the virtual + * file system + * @param executableName name of the executable + */ + DLLEXPORT VOID WINAPI usvfsBlacklistExecutable(LPCWSTR executableName); + + /** + * clears the executable blacklist + */ + DLLEXPORT VOID WINAPI usvfsClearExecutableBlacklist(); + + /** + * adds a file suffix to a list to skip during file linking + * .txt and some_file.txt are both valid file suffixes, + * not to be confused with file extensions + * @param fileSuffix a valid file suffix + */ + DLLEXPORT VOID WINAPI usvfsAddSkipFileSuffix(LPCWSTR fileSuffix); + + /** + * clears the file suffix skip-list + */ + DLLEXPORT VOID WINAPI usvfsClearSkipFileSuffixes(); + + /** + * adds a directory name that will be skipped during directory linking. + * Not a path. Any directory matching the name will be skipped, + * regardless of it's path, for example if .git is added, + * any sub-path or root-path containing a .git directory + * will have the .git directory skipped during directory linking + * @param directory name of the directory + */ + DLLEXPORT VOID WINAPI usvfsAddSkipDirectory(LPCWSTR directory); + + /** + * clears the directory skip-list + */ + DLLEXPORT VOID WINAPI usvfsClearSkipDirectories(); + + /** + * adds a library to be force loaded when the given process is injected + * @param + */ + DLLEXPORT VOID WINAPI usvfsForceLoadLibrary(LPCWSTR processName, LPCWSTR libraryPath); + + /** + * clears all previous calls to ForceLoadLibrary + */ + DLLEXPORT VOID WINAPI usvfsClearLibraryForceLoads(); + + /** + * print debugging info about the vfs. The format is currently not fixed and may + * change between usvfs versions + */ + DLLEXPORT VOID WINAPI usvfsPrintDebugInfo(); + + // #if defined(UNITTEST) || defined(_WINDLL) + DLLEXPORT void WINAPI usvfsInitLogging(bool toLocal = false); + // #endif + + /** + * used internally to initialize a process at startup-time as a "slave". Don't call + * directly + */ + DLLEXPORT void __cdecl InitHooks(LPVOID userData, size_t userDataSize); + + // the instance and shm names are not updated + // + DLLEXPORT void WINAPI usvfsUpdateParameters(usvfsParameters* p); + + DLLEXPORT int WINAPI usvfsCreateMiniDump(PEXCEPTION_POINTERS exceptionPtrs, + CrashDumpsType type, + const wchar_t* dumpPath); + + DLLEXPORT const char* WINAPI usvfsVersionString(); +} diff --git a/libs/usvfs/include/usvfs/usvfs_version.h b/libs/usvfs/include/usvfs/usvfs_version.h new file mode 100644 index 0000000..03e0cf0 --- /dev/null +++ b/libs/usvfs/include/usvfs/usvfs_version.h @@ -0,0 +1,30 @@ +#pragma once + +#define USVFS_VERSION_MAJOR 0 +#define USVFS_VERSION_MINOR 5 +#define USVFS_VERSION_BUILD 7 +#define USVFS_VERSION_REVISION 2 + +#define USVFS_BUILD_STRING "" +#define USVFS_BUILD_WSTRING L"" + +#ifndef USVFS_STRINGIFY +#define USVFS_STRINGIFY(x) USVFS_STRINGIFY_HELPER(x) +#define USVFS_STRINGIFY_HELPER(x) #x +#endif + +#ifndef USVFS_STRINGIFYW +#define USVFS_STRINGIFYW(x) USVFS_STRINGIFYW_HELPER(x) +#define USVFS_STRINGIFYW_HELPER(x) L## #x +#endif + +#define USVFS_VERSION_STRING \ + USVFS_STRINGIFY(USVFS_VERSION_MAJOR) \ + "." USVFS_STRINGIFY(USVFS_VERSION_MINOR) "." USVFS_STRINGIFY( \ + USVFS_VERSION_BUILD) "." USVFS_STRINGIFY(USVFS_VERSION_REVISION) \ + USVFS_BUILD_STRING +#define USVFS_VERSION_WSTRING \ + USVFS_STRINGIFYW(USVFS_VERSION_MAJOR) \ + L"." USVFS_STRINGIFYW(USVFS_VERSION_MINOR) L"." USVFS_STRINGIFYW( \ + USVFS_VERSION_BUILD) L"." USVFS_STRINGIFYW(USVFS_VERSION_REVISION) \ + USVFS_BUILD_WSTRING diff --git a/libs/usvfs/include/usvfs/usvfsparameters.h b/libs/usvfs/include/usvfs/usvfsparameters.h new file mode 100644 index 0000000..1351d69 --- /dev/null +++ b/libs/usvfs/include/usvfs/usvfsparameters.h @@ -0,0 +1,68 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "dllimport.h" +#include "logging.h" +#include + +enum class CrashDumpsType : uint8_t +{ + None, + Mini, + Data, + Full +}; + +extern "C" +{ + + // deprecated, use usvfsParameters and usvfsCreateParameters() + // + struct USVFSParameters + { + char instanceName[65]; + char currentSHMName[65]; + char currentInverseSHMName[65]; + bool debugMode{false}; + LogLevel logLevel{LogLevel::Debug}; + CrashDumpsType crashDumpsType{CrashDumpsType::None}; + char crashDumpsPath[260]; + }; + + struct usvfsParameters; + + DLLEXPORT usvfsParameters* usvfsCreateParameters(); + DLLEXPORT usvfsParameters* usvfsDupeParameters(usvfsParameters* p); + DLLEXPORT void usvfsCopyParameters(const usvfsParameters* source, + usvfsParameters* dest); + DLLEXPORT void usvfsFreeParameters(usvfsParameters* p); + + DLLEXPORT void usvfsSetInstanceName(usvfsParameters* p, const char* name); + DLLEXPORT void usvfsSetDebugMode(usvfsParameters* p, BOOL debugMode); + DLLEXPORT void usvfsSetLogLevel(usvfsParameters* p, LogLevel level); + DLLEXPORT void usvfsSetCrashDumpType(usvfsParameters* p, CrashDumpsType type); + DLLEXPORT void usvfsSetCrashDumpPath(usvfsParameters* p, const char* path); + DLLEXPORT void usvfsSetProcessDelay(usvfsParameters* p, int milliseconds); + + DLLEXPORT const char* usvfsLogLevelToString(LogLevel lv); + DLLEXPORT const char* usvfsCrashDumpTypeToString(CrashDumpsType t); +} diff --git a/libs/usvfs/include/usvfs/usvfsparametersprivate.h b/libs/usvfs/include/usvfs/usvfsparametersprivate.h new file mode 100644 index 0000000..6c2d5f2 --- /dev/null +++ b/libs/usvfs/include/usvfs/usvfsparametersprivate.h @@ -0,0 +1,32 @@ +#pragma once +#include "usvfsparameters.h" + +struct usvfsParameters +{ + char instanceName[65]; + char currentSHMName[65]; + char currentInverseSHMName[65]; + bool debugMode; + LogLevel logLevel{LogLevel::Debug}; + CrashDumpsType crashDumpsType{CrashDumpsType::None}; + char crashDumpsPath[260]; + int delayProcessMs; + + usvfsParameters(); + usvfsParameters(const usvfsParameters&) = default; + usvfsParameters& operator=(const usvfsParameters&) = default; + + usvfsParameters(const char* instanceName, const char* currentSHMName, + const char* currentInverseSHMName, bool debugMode, LogLevel logLevel, + CrashDumpsType crashDumpsType, const char* crashDumpsPath, + int delayProcessMs); + + usvfsParameters(const USVFSParameters& oldParams); + + void setInstanceName(const char* name); + void setDebugMode(bool debugMode); + void setLogLevel(LogLevel level); + void setCrashDumpType(CrashDumpsType type); + void setCrashDumpPath(const char* path); + void setProcessDelay(int milliseconds); +}; diff --git a/libs/usvfs/licenses/asmjit.txt b/libs/usvfs/licenses/asmjit.txt new file mode 100644 index 0000000..e340bde --- /dev/null +++ b/libs/usvfs/licenses/asmjit.txt @@ -0,0 +1,18 @@ +AsmJit - Complete x86/x64 JIT and Remote Assembler for C++ +Copyright (c) 2008-2015, Petr Kobalicek + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/libs/usvfs/licenses/boost.txt b/libs/usvfs/licenses/boost.txt new file mode 100644 index 0000000..36b7cd9 --- /dev/null +++ b/libs/usvfs/licenses/boost.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/libs/usvfs/licenses/cppformat.txt b/libs/usvfs/licenses/cppformat.txt new file mode 100644 index 0000000..8db9a7c --- /dev/null +++ b/libs/usvfs/licenses/cppformat.txt @@ -0,0 +1,9 @@ +Copyright (c) 2012 - 2015, Victor Zverovich + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libs/usvfs/licenses/googletest.txt b/libs/usvfs/licenses/googletest.txt new file mode 100644 index 0000000..1941a11 --- /dev/null +++ b/libs/usvfs/licenses/googletest.txt @@ -0,0 +1,28 @@ +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libs/usvfs/licenses/qt.txt b/libs/usvfs/licenses/qt.txt new file mode 100644 index 0000000..87be995 --- /dev/null +++ b/libs/usvfs/licenses/qt.txt @@ -0,0 +1,174 @@ + GNU LESSER GENERAL PUBLIC LICENSE + + The Qt Toolkit is Copyright (C) 2015 The Qt Company Ltd. + Contact: http://www.qt.io/licensing/ + + You may use, distribute and copy the Qt GUI Toolkit under the terms of + GNU Lesser General Public License version 3, which is displayed below. + This license makes reference to the version 3 of the GNU General + Public License, which you can find in the LICENSE.GPLv3 file. + +------------------------------------------------------------------------- + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright © 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies of this +licensedocument, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + +0. Additional Definitions. + + As used herein, “this License” refers to version 3 of the GNU Lesser +General Public License, and the “GNU GPL” refers to version 3 of the +GNU General Public License. + + “The Library” refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An “Application” is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A “Combined Work” is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the “Linked +Version”. + + The “Minimal Corresponding Source” for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The “Corresponding Application Code” for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort + to ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + +3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this + license document. + +4. Combined Works. + + You may convey a Combined Work under terms of your choice that, taken +together, effectively do not restrict modification of the portions of +the Library contained in the Combined Work and reverse engineering for +debugging such modifications, if you also do each of the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this + license document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of + this License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with + the Library. A suitable mechanism is one that (a) uses at run + time a copy of the Library already present on the user's + computer system, and (b) will operate properly with a modified + version of the Library that is interface-compatible with the + Linked Version. + + e) Provide Installation Information, but only if you would + otherwise be required to provide such information under section 6 + of the GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the Application + with a modified version of the Linked Version. (If you use option + 4d0, the Installation Information must accompany the Minimal + Corresponding Source and Corresponding Application Code. If you + use option 4d1, you must provide the Installation Information in + the manner specified by section 6 of the GNU GPL for conveying + Corresponding Source.) + +5. Combined Libraries. + + You may place library facilities that are a work based on the Library +side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities, conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of + it is a work based on the Library, and explaining where to find + the accompanying uncombined form of the same work. + +6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +as you received it specifies that a certain numbered version of the +GNU Lesser General Public License “or any later version” applies to +it, you have the option of following the terms and conditions either +of that published version or of any later version published by the +Free Software Foundation. If the Library as you received it does not +specify a version number of the GNU Lesser General Public License, +you may choose any version of the GNU Lesser General Public License +ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the Library. diff --git a/libs/usvfs/licenses/spdlog.txt b/libs/usvfs/licenses/spdlog.txt new file mode 100644 index 0000000..1dfac98 --- /dev/null +++ b/libs/usvfs/licenses/spdlog.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Gabi Melman. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/libs/usvfs/licenses/udis86.txt b/libs/usvfs/licenses/udis86.txt new file mode 100644 index 0000000..80f537d --- /dev/null +++ b/libs/usvfs/licenses/udis86.txt @@ -0,0 +1,22 @@ +Copyright (c) 2002-2012, Vivek Thampi +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libs/usvfs/src/shared/CMakeLists.txt b/libs/usvfs/src/shared/CMakeLists.txt new file mode 100644 index 0000000..b1a7c05 --- /dev/null +++ b/libs/usvfs/src/shared/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Boost CONFIG REQUIRED COMPONENTS algorithm interprocess filesystem) +find_package(spdlog CONFIG REQUIRED) + +file(GLOB sources "*.cpp" "*.h") +source_group("" FILES ${sources}) + +add_library(shared STATIC ${sources}) +target_link_libraries(shared + PUBLIC + Boost::algorithm Boost::interprocess comsuppw + Boost::filesystem spdlog::spdlog_header_only) +target_include_directories(shared + PUBLIC ${PROJECT_SOURCE_DIR}/include/usvfs ${CMAKE_CURRENT_SOURCE_DIR}) +target_precompile_headers(shared PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/pch.h) +target_compile_options(shared PUBLIC /FI"pch.h") +target_compile_definitions(shared PUBLIC + SPDLOG_USE_STD_FORMAT SPDLOG_NO_NAME SPDLOG_NO_REGISTRY_MUTEX + BOOST_INTERPROCESS_SHARED_DIR_FUNC BOOST_CONFIG_SUPPRESS_OUTDATED_MESSAGE + NOMINMAX _WINDOWS _UNICODE UNICODE + _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR +) diff --git a/libs/usvfs/src/shared/addrtools.h b/libs/usvfs/src/shared/addrtools.h new file mode 100644 index 0000000..b888ebf --- /dev/null +++ b/libs/usvfs/src/shared/addrtools.h @@ -0,0 +1,64 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "windows_sane.h" + +namespace usvfs::shared +{ + +#ifdef _M_AMD64 +typedef DWORD64 REGWORD; +#elif _M_IX86 +typedef DWORD REGWORD; +#endif + +inline LPVOID AddrAdd(LPVOID address, size_t offset) +{ + return reinterpret_cast(reinterpret_cast(address) + offset); +} + +inline std::ptrdiff_t AddrDiff(LPVOID lhs, LPVOID rhs) +{ + return reinterpret_cast(lhs) - reinterpret_cast(rhs); +} + +// implicitly cast pointer to void*, from there cast to target type. +// This is supposed to be safer than directly reinterpret-casting +template +inline T void_ptr_cast(void* ptr) +{ + return reinterpret_cast(ptr); +} + +template <> +inline int64_t void_ptr_cast(void* ptr) +{ + return reinterpret_cast(ptr); +} + +template <> +inline uint64_t void_ptr_cast(void* ptr) +{ + return reinterpret_cast(ptr); +} + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/directory_tree.cpp b/libs/usvfs/src/shared/directory_tree.cpp new file mode 100644 index 0000000..34cc797 --- /dev/null +++ b/libs/usvfs/src/shared/directory_tree.cpp @@ -0,0 +1,43 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "directory_tree.h" +#include "tree_container.h" + +namespace usvfs::shared +{ + +fs::path::iterator nextIter(const fs::path::iterator& iter, + const fs::path::iterator& end) +{ + fs::path::iterator next = iter; + advanceIter(next, end); + return next; +} + +void advanceIter(fs::path::iterator& iter, const fs::path::iterator& end) +{ + ++iter; + while (iter != end && (iter->wstring() == L"/" || iter->wstring() == L"\\" || + iter->wstring() == L".")) + ++iter; +} + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/directory_tree.h b/libs/usvfs/src/shared/directory_tree.h new file mode 100644 index 0000000..f82187f --- /dev/null +++ b/libs/usvfs/src/shared/directory_tree.h @@ -0,0 +1,629 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "exceptionex.h" +#include "logging.h" +#include "shared_memory.h" +#include "stringutils.h" +#include "wildcard.h" + +// simplify unit tests by allowing access to private members +#ifndef PRIVATE +#define PRIVATE private +#endif // PRIVATE + +namespace usvfs::shared +{ + +template +struct SHMDataCreator +{ + static T create(const U& source, const VoidAllocatorT& allocator) + { + return T(source, allocator); + } +}; + +template +T createData(const U& source, const VoidAllocatorT& allocator) +{ + return SHMDataCreator::create(source, allocator); +} + +template +T createDataEmpty(const VoidAllocatorT& allocator); + +template +void dataAssign(T& destination, const T& source); + +// crappy little workaround for fs::path iterating over path separators +fs::path::iterator nextIter(const fs::path::iterator& iter, + const fs::path::iterator& end); + +void advanceIter(fs::path::iterator& iter, const fs::path::iterator& end); + +// decomposes a path into its components +// +class DecomposablePath +{ +public: + explicit DecomposablePath(std::string s) : m_s(std::move(s)), m_begin(0), m_end(0) + { + m_end = nextSeparator(m_begin); + } + + // move to the next component, returns false when there are no more components + // + bool next() + { + for (;;) { + if (m_end >= m_s.size()) { + // done + m_begin = m_end; + return false; + } + + // move begin to one past the last separator found + m_begin = m_end + 1; + + // find the next separator + m_end = nextSeparator(m_begin); + + // check for components that should be ignored: + // - empty, happens when the path ends with a separator + // - slashes, happens with consecutive separators + // - dot, unnecessary + const auto c = current(); + if (!c.empty() && c != "\\" && c != "/" && c != ".") { + return true; + } + } + } + + // checks if next() would return false + // + bool peekNext() const + { + auto copy = *this; + return copy.next(); + } + + // the current component, empty when next() returned false + // + std::string_view current() const { return {m_s.data() + m_begin, m_end - m_begin}; } + +private: + const std::string m_s; + std::size_t m_begin, m_end; + + // finds the next path separator + // + std::size_t nextSeparator(std::size_t from) const + { + while (from < m_s.size()) { + if (m_s[from] == '/' || m_s[from] == '\\') { + break; + } + + ++from; + } + + return from; + } +}; + +namespace bi = boost::interprocess; +namespace bmi = boost::multi_index; + +typedef uint8_t TreeFlags; + +static const TreeFlags FLAG_DIRECTORY = 0x01; +static const TreeFlags FLAG_DUMMY = 0x02; +static const TreeFlags FLAG_FIRSTUSERFLAG = 0x10; + +struct MissingThrowT +{}; +static const MissingThrowT MissingThrow = MissingThrowT(); + +template +class TreeContainer; + +template +struct mutable_pair +{ + typedef T1 first_type; + typedef T2 second_type; + + mutable_pair(Alloc alloc) : first(T1(alloc)), second(T2(alloc)) {} + + mutable_pair(const T1& f, const T2& s) : first(f), second(s) {} + + mutable_pair(const std::pair& p) : first(p.first), second(p.second) {} + + T1 first; + mutable T2 second; +}; + +template > +using mimap = bmi::multi_index_container< + Element, + bmi::indexed_by< + bmi::ordered_unique, Compare>>, + typename Allocator::template rebind::other>; + +/** + * a representation of a directory tree in memory. + * This class is designed to be stored in shared memory. + */ +template +class DirectoryTree +{ + template + friend class TreeContainer; + +public: + struct CILess + { + template + bool operator()(const U& lhs, const V& rhs) const + { + const size_t lhsLength = getLength(lhs); + const size_t rhsLength = getLength(rhs); + + const auto r = + _strnicmp(getCharPtr(lhs), getCharPtr(rhs), std::min(lhsLength, rhsLength)); + + if (r == 0) { + return lhsLength < rhsLength; + } + + return (r < 0); + } + + private: + const char* getCharPtr(const StringT& s) const { return s.c_str(); } + + const char* getCharPtr(const std::string& s) const { return s.c_str(); } + + const char* getCharPtr(const char* s) const { return s; } + + const char* getCharPtr(std::string_view s) const { return s.data(); } + + size_t getLength(const StringT& s) const { return s.size(); } + + size_t getLength(const std::string& s) const { return s.size(); } + + size_t getLength(const char* s) const { return strlen(s); } + + size_t getLength(std::string_view s) const { return s.size(); } + }; + + typedef DirectoryTree NodeT; + typedef bi::deleter DeleterT; + typedef NodeDataT DataT; + + typedef bi::shared_ptr NodePtrT; + typedef bi::weak_ptr WeakPtrT; + + typedef bi::allocator, SegmentManagerT> + NodeEntryAllocatorT; + + typedef mimap NodeMapT; + typedef typename NodeMapT::iterator file_iterator; + typedef typename NodeMapT::const_iterator const_file_iterator; + + typedef std::function VisitorFunction; + + DirectoryTree() = delete; + DirectoryTree(const NodeT& reference) = delete; + DirectoryTree(NodeT&& reference) = delete; + NodeT& operator=(NodeT reference) = delete; + + /** + * @brief construct a new node to be inserted in an existing tree + **/ + DirectoryTree(std::string_view name, TreeFlags flags, const NodePtrT& parent, + const NodeDataT& data, const VoidAllocatorT& allocator) + : m_Parent(parent), m_Name(name.begin(), name.end(), allocator), m_Data(data), + m_Nodes(allocator), m_Flags(flags) + {} + + ~DirectoryTree() { m_Nodes.clear(); } + + /** + * @return parent node + */ + NodePtrT parent() const { return m_Parent.lock(); } + + /** + * @return the full path to the node + */ + fs::path path() const + { + if (m_Parent.lock().get() == nullptr) { + if (m_Name.size() == 0) { + return fs::path(); + } else { + return fs::path(m_Name.c_str()) / "\\"; + } + } else { + return m_Parent.lock()->path() / m_Name.c_str(); + } + } + + /** + * @return data connected to this node + **/ + const NodeDataT& data() const { return m_Data; } + + /** + * @return name of this node + */ + std::string name() const { return m_Name.c_str(); } + + /** + * @brief setFlag change a flag for this node + * @param enabled new state for the specified flag + */ + void setFlag(TreeFlags flag, bool enabled = true) + { + m_Flags = enabled ? m_Flags | flag : m_Flags & ~flag; + } + + /** + * @return true if the specified flag is set, false otherwise + */ + bool hasFlag(TreeFlags flag) const { return (m_Flags & flag) != 0; } + + /** + * @return true if this node is a directory, false if it's a regular file + */ + bool isDirectory() const { return hasFlag(FLAG_DIRECTORY); } + + /** + * @return the number of subnodes (directly) below this one + */ + size_t numNodes() const { return m_Nodes.size(); } + + /** + * @return number of nodes in this (sub-)tree including this one + */ + size_t numNodesRecursive() const + { + size_t result = numNodes() + 1; + + for (const auto& node : m_Nodes) { + result += node.second->numNodesRecursive(); + } + + return result; + } + + /** + * @brief find a node by its path + * @param path the path to look up + * @return a pointer to the node or a null ptr + */ + NodePtrT findNode(const fs::path& path) + { + fs::path::iterator iter = path.begin(); + return findNode(path, iter); + } + + /** + * @brief find a node by its path + * @param path the path to look up + * @return a pointer to the node or a null ptr + */ + const NodePtrT findNode(const fs::path& path) const + { + fs::path::iterator iter = path.begin(); + return findNode(path, iter); + } + + /** + * @brief visit the nodes along the specified path (in order) calling the visitor for + * each + * @param path the path to visit + * @param visitor a function called for each node + */ + void visitPath(const fs::path& path, const VisitorFunction& visitor) const + { + fs::path::iterator iter = path.begin(); + visitPath(path, iter, visitor); + } + + /** + * @brief retrieve a node by the specified name + * @param name name of the node + * @return the node found or an empty pointer if no such node was found + */ + NodePtrT node(std::string_view name, MissingThrowT) const + { + auto iter = m_Nodes.find(name); + + if (iter != m_Nodes.end()) { + return iter->second; + } else { + USVFS_THROW_EXCEPTION(node_missing_error()); + } + } + + /** + * @brief retrieve a node by the specified name + * @param name name of the node + * @return the node found or an empty pointer if no such node was found + */ + NodePtrT node(std::string_view name) + { + auto iter = m_Nodes.find(name); + + if (iter != m_Nodes.end()) { + return iter->second; + } else { + return NodePtrT(); + } + } + + /** + * @brief retrieve a node by the specified name + * @param name name of the node + * @return the node found or an empty pointer if no such node was found + */ + const NodePtrT node(std::string_view name, MissingThrowT) + { + auto iter = m_Nodes.find(name); + + if (iter != m_Nodes.end()) { + return iter->second; + } else { + USVFS_THROW_EXCEPTION(node_missing_error()); + } + } + + /** + * @brief retrieve a node by the specified name + * @param name name of the node + * @return the node found or an empty pointer if no such node was found + */ + const NodePtrT node(std::string_view name) const + { + auto iter = m_Nodes.find(name); + + if (iter != m_Nodes.end()) { + return iter->second; + } else { + return NodePtrT(); + } + } + + /** + * @brief test if a node by the specified name exists + * @param name name of the node + * @return true if the node exists, false otherwise + */ + bool exists(std::string_view name) const + { + return m_Nodes.find(name) != m_Nodes.end(); + } + + /** + * @brief find all matches for a pattern + * @param pattern the pattern to look for + * @return a vector of the found nodes + */ + std::vector find(const std::string& pattern) const + { + // determine if there is a prefix in the pattern that indicates a specific + // directory. + size_t fixedPart = pattern.find_first_of("*?"); + + if (fixedPart == 0) + fixedPart = std::string::npos; + if (fixedPart != std::string::npos) + fixedPart = pattern.find_last_of(R"(\/)", fixedPart); + + std::vector result; + + if (fixedPart != std::string::npos) { + // if there is a prefix, search for the node representing that path and + // search only on that + NodePtrT node = findNode(fs::path(pattern.substr(0, fixedPart))); + if (node.get() != nullptr) { + node->findLocal(result, pattern.substr(fixedPart + 1)); + } + } else { + findLocal(result, pattern); + } + + return result; + } + + /** + * @return an iterator to the first leaf + **/ + file_iterator filesBegin() { return m_Nodes.begin(); } + + /** + * @return a const iterator to the first leaf + **/ + const_file_iterator filesBegin() const { return m_Nodes.begin(); } + + /** + * @return an iterator one past the last leaf + **/ + file_iterator filesEnd() { return m_Nodes.end(); } + + /** + * @return a const iterator one past the last leaf + **/ + const_file_iterator filesEnd() const { return m_Nodes.end(); } + + /** + * @brief erase the leaf at the specified iterator + * @return an iterator to the following file + **/ + file_iterator erase(file_iterator iter) { return m_Nodes.erase(iter); } + + /** + * @brief clear all nodes + */ + void clear() { m_Nodes.clear(); } + + void removeFromTree() + { + if (auto par = parent()) { + spdlog::get("usvfs")->info("remove from tree {}", m_Name.c_str()); + auto self = par->m_Nodes.find(m_Name.c_str()); + if (self != par->m_Nodes.end()) { + par->erase(self); + } else { + // trying to remove a node that does not exist, most likely because it was + // already removed in a lower level call. this is known to happen when MoveFile + // has the MOVEFILE_COPY_ALLOWED flag and moving a mapped file. + spdlog::get("usvfs")->warn("Failed to remove inexisting node from tree: {}", + m_Name.c_str()); + } + } + } + + PRIVATE : void set(StringT key, const NodePtrT& value) + { + auto res = m_Nodes.emplace(std::move(key), value); + if (!res.second) { + res.first->second = value; + } + } + + WeakPtrT findRoot() const + { + if (m_Parent.lock().get() == nullptr) { + return m_Self; + } else { + return m_Parent.lock()->findRoot(); + } + } + + NodePtrT findNode(const fs::path& name, fs::path::iterator& iter) + { + std::string l = iter->string(); + auto subNode = m_Nodes.find(iter->string()); + advanceIter(iter, name.end()); + + if (iter == name.end()) { + // last name component, should be a local node + if (subNode != m_Nodes.end()) { + return subNode->second; + } else { + return NodePtrT(); + } + } else { + if (subNode != m_Nodes.end()) { + return subNode->second->findNode(name, iter); + } else { + return NodePtrT(); + } + } + } + + const NodePtrT findNode(const fs::path& name, fs::path::iterator& iter) const + { + auto subNode = m_Nodes.find(iter->string()); + advanceIter(iter, name.end()); + + if (iter == name.end()) { + // last name component, should be a local node + if (subNode != m_Nodes.end()) { + return subNode->second; + } else { + return NodePtrT(); + } + } else { + if (subNode != m_Nodes.end()) { + return subNode->second->findNode(name, iter); + } else { + return NodePtrT(); + } + } + } + + void visitPath(const fs::path& path, fs::path::iterator& iter, + const VisitorFunction& visitor) const + { + auto subNode = m_Nodes.find(iter->string()); + + if (subNode != m_Nodes.end()) { + visitor(subNode->second); + advanceIter(iter, path.end()); + if (iter != path.end()) { + subNode->second->visitPath(path, iter, visitor); + } + } + } + + void findLocal(std::vector& output, const std::string& pattern) const + { + for (auto iter = m_Nodes.begin(); iter != m_Nodes.end(); ++iter) { + LPCSTR remainder = nullptr; + + if (pattern.size() > 1 && (pattern[0] == '*') && + ((pattern[1] == '/') || (pattern[1] == '\\')) && + iter->second->isDirectory()) { + // the star may represent a directory (one directory level, not + // multiple!), search in subdirectory + iter->second->findLocal(output, pattern.substr(1)); + } else if ((remainder = wildcard::PartialMatch(iter->second->name().c_str(), + pattern.c_str())) != nullptr) { + if ((*remainder == '\0') || (strcmp(remainder, "*") == 0)) { + NodePtrT node = iter->second; + output.push_back(node); + } + + if (iter->second->isDirectory()) { + iter->second->findLocal(output, remainder); + } + } + } + } + + PRIVATE : TreeFlags m_Flags; + + WeakPtrT m_Parent; + WeakPtrT m_Self; + + StringT m_Name; + NodeDataT m_Data; + + NodeMapT m_Nodes; +}; + +template +void dumpTree(std::ostream& stream, const DirectoryTree& tree, int level = 0) +{ + stream << std::string(level, ' ') << tree.name() << " -> " << tree.data() << "\n"; + for (auto iter = tree.filesBegin(); iter != tree.filesEnd(); ++iter) { + dumpTree(stream, *iter->second, level + 1); + } +} + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/exceptionex.cpp b/libs/usvfs/src/shared/exceptionex.cpp new file mode 100644 index 0000000..4a814b5 --- /dev/null +++ b/libs/usvfs/src/shared/exceptionex.cpp @@ -0,0 +1,82 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "exceptionex.h" +#include "winapi.h" +#include + +namespace usvfs::shared +{ + +std::string windows_error::constructMessage(const std::string& input, int inErrorCode) +{ + std::ostringstream finalMessage; + finalMessage << input; + + LPSTR buffer = nullptr; + + DWORD errorCode = inErrorCode != -1 ? inErrorCode : GetLastError(); + + // TODO: the message is not english? + if (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + nullptr, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&buffer, 0, nullptr) == 0) { + finalMessage << " (errorcode " << errorCode << ")"; + } else { + LPSTR lastChar = buffer + strlen(buffer) - 2; + *lastChar = '\0'; + finalMessage << " (" << buffer << " [" << errorCode << "])"; + LocalFree(buffer); // allocated by FormatMessage + } + + SetLastError( + errorCode); // restore error code because FormatMessage might have modified it + return finalMessage.str(); +} + +} // namespace usvfs::shared + +void logExtInfo(const std::exception& e, LogLevel logLevel) +{ + std::string content; + + if (const std::string* msg = boost::get_error_info(e)) { + content = *msg; + } + + if (const DWORD* errorCode = boost::get_error_info(e)) { + content = std::string("error: ") + winapi::ex::ansi::errorString(*errorCode); + } + + switch (logLevel) { + case LogLevel::Debug: + spdlog::get("usvfs")->debug(content); + break; + case LogLevel::Info: + spdlog::get("usvfs")->info(content); + break; + case LogLevel::Warning: + spdlog::get("usvfs")->warn(content); + break; + case LogLevel::Error: + spdlog::get("usvfs")->error(content); + break; + } +} diff --git a/libs/usvfs/src/shared/exceptionex.h b/libs/usvfs/src/shared/exceptionex.h new file mode 100644 index 0000000..4c298a5 --- /dev/null +++ b/libs/usvfs/src/shared/exceptionex.h @@ -0,0 +1,99 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "logging.h" +#include +#include + +typedef boost::error_info ex_win_errcode; +typedef boost::error_info ex_msg; + +struct std_boost_exception : virtual boost::exception, virtual std::exception +{ + const char* what() const noexcept override + { + return boost::diagnostic_information_what(*this); + } +}; + +struct incompatibility_error : std_boost_exception +{}; +struct usage_error : std_boost_exception +{}; +struct data_error : std_boost_exception +{}; +struct file_not_found_error : std_boost_exception +{}; +struct timeout_error : std_boost_exception +{}; +struct unknown_error : std_boost_exception +{}; +struct node_missing_error : std_boost_exception +{}; + +#define USVFS_THROW_EXCEPTION(x) BOOST_THROW_EXCEPTION(x) + +void logExtInfo(const std::exception& e, LogLevel logLevel = LogLevel::Warning); + +namespace usvfs::shared +{ + +class windows_error : public std::runtime_error +{ +public: + windows_error(const std::string& message, int errorcode = GetLastError()) + : runtime_error(constructMessage(message, errorcode)), m_ErrorCode(errorcode) + {} + + int getErrorCode() const { return m_ErrorCode; } + +private: + int m_ErrorCode; + + std::string constructMessage(const std::string& input, int errorcode); +}; + +class guard +{ +public: + explicit guard(std::function f) : m_f(std::move(f)) {} + + ~guard() + { + if (m_f) { + m_f(); + } + } + + guard(const guard&); + guard& operator=(const guard&); + +private: + std::function m_f; +}; + +} // namespace usvfs::shared + +#define CONCATENATE_DIRECT(s1, s2) s1##s2 +#define CONCATENATE(s1, s2) CONCATENATE_DIRECT(s1, s2) +#define ANONYMOUS_VARIABLE(str) CONCATENATE(str, __LINE__) +#define ON_BLOCK_EXIT(f) usvfs::shared::guard ANONYMOUS_VARIABLE(guard)(f) diff --git a/libs/usvfs/src/shared/formatters.h b/libs/usvfs/src/shared/formatters.h new file mode 100644 index 0000000..3d0e47e --- /dev/null +++ b/libs/usvfs/src/shared/formatters.h @@ -0,0 +1,158 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2024. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include +#include + +#include "ntdll_declarations.h" + +// formatters for standard types + +namespace usvfs::log +{ +std::string to_string(LPCWSTR value); +std::string to_string(PCUNICODE_STRING value); +} // namespace usvfs::log + +template + requires std::is_enum_v +struct std::formatter : std::formatter, CharT> +{ + template + FmtContext::iterator format(Enum v, FmtContext& ctx) const + { + return std::formatter, CharT>::format( + static_cast>(v), ctx); + } +}; + +template <> +struct std::formatter : std::formatter +{ + template + FmtContext::iterator format(LPCWSTR v, FmtContext& ctx) const + { + return std::formatter::format(usvfs::log::to_string(v), ctx); + } +}; + +template <> +struct std::formatter : std::formatter +{ + template + FmtContext::iterator format(const std::wstring& v, FmtContext& ctx) const + { + return std::formatter::format(v.c_str(), ctx); + } +}; + +template <> +struct std::formatter : std::formatter +{ + template + FmtContext::iterator format(PCUNICODE_STRING v, FmtContext& ctx) const + { + return std::formatter::format(usvfs::log::to_string(v), ctx); + } +}; + +template <> +struct std::formatter : std::formatter +{ + template + FmtContext::iterator format(UNICODE_STRING v, FmtContext& ctx) const + { + return std::formatter::format(usvfs::log::to_string(&v), ctx); + } +}; + +template + requires(std::is_pointer_v && !std::is_same_v && + !std::is_same_v && !std::is_same_v && + !std::is_same_v) +struct std::formatter : std::formatter +{ + template + FmtContext::iterator format(Pointer v, FmtContext& ctx) const + { + return std::formatter::format(v, ctx); + } +}; + +namespace usvfs::log +{ + +/** + * a small helper class to wrap any object. The whole point is to give us a way + * to ensure our own operator<< is used in addParam calls + */ +template +class Wrap +{ +public: + explicit Wrap(const T& data) : m_Data(data) {} + Wrap(Wrap&& reference) : m_Data(std::move(reference.m_Data)) {} + + Wrap(const Wrap& reference) = delete; + Wrap& operator=(const Wrap& reference) = delete; + +private: + friend struct ::std::formatter, char>; + const T& m_Data; +}; + +template +Wrap wrap(const T& data) +{ + return Wrap(data); +} + +} // namespace usvfs::log + +template <> +struct std::formatter, char> : std::formatter +{ + template + FmtContext::iterator format(const usvfs::log::Wrap& v, FmtContext& ctx) const + { + return std::format_to(ctx.out(), "{:x}", v.m_Data); + } +}; + +template <> +struct std::formatter, char> : std::formatter +{ + template + FmtContext::iterator format(const usvfs::log::Wrap& v, + FmtContext& ctx) const + { + switch (v.m_Data) { + case 0x00000000: + return std::format_to(ctx.out(), "ok"); + case 0xC0000022: + return std::format_to(ctx.out(), "access denied"); + case 0xC0000035: + return std::format_to(ctx.out(), "exists already"); + } + return std::format_to(ctx.out(), "err {:x}", v.m_Data); + } +}; diff --git a/libs/usvfs/src/shared/loghelpers.cpp b/libs/usvfs/src/shared/loghelpers.cpp new file mode 100644 index 0000000..bfcabf1 --- /dev/null +++ b/libs/usvfs/src/shared/loghelpers.cpp @@ -0,0 +1,96 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include + +#include "formatters.h" +#include "loghelpers.h" +#include "stringcast.h" +#include "stringutils.h" + +namespace ush = usvfs::shared; + +namespace usvfs::log +{ + +// this is declared in formatters but is defined here to avoid a whole .cpp +// file for two small functions +// +// comments from old code, taken as is: +// +// TODO this does not correctly support surrogate pairs since the size used here +// is the number of 16-bit characters in the buffer whereas toNarrow expects the +// actual number of characters. It will always underestimate though, so worst +// case scenario we truncate the string +// +std::string to_string(LPCWSTR value) +{ + if (value == nullptr) { + return ""; + } + try { + return ush::string_cast(value, ush::CodePage::UTF8); + } catch (const std::exception& e) { + return std::format("", e.what()); + } +} +std::string to_string(PCUNICODE_STRING value) +{ + try { + return ush::string_cast(value->Buffer, ush::CodePage::UTF8, + value->Length / sizeof(WCHAR)); + } catch (const std::exception& e) { + return std::format("", e.what()); + } +} + +spdlog::level::level_enum ConvertLogLevel(LogLevel level) +{ + switch (level) { + case LogLevel::Debug: + return spdlog::level::debug; + case LogLevel::Info: + return spdlog::level::info; + case LogLevel::Warning: + return spdlog::level::warn; + case LogLevel::Error: + return spdlog::level::err; + default: + return spdlog::level::debug; + } +} + +LogLevel ConvertLogLevel(spdlog::level::level_enum level) +{ + switch (level) { + case spdlog::level::debug: + return LogLevel::Debug; + case spdlog::level::info: + return LogLevel::Info; + case spdlog::level::warn: + return LogLevel::Warning; + case spdlog::level::err: + return LogLevel::Error; + default: + return LogLevel::Debug; + } +} + +} // namespace usvfs::log diff --git a/libs/usvfs/src/shared/loghelpers.h b/libs/usvfs/src/shared/loghelpers.h new file mode 100644 index 0000000..071ad9d --- /dev/null +++ b/libs/usvfs/src/shared/loghelpers.h @@ -0,0 +1,123 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "dllimport.h" +#include "formatters.h" +#include "ntdll_declarations.h" +#include "shmlogger.h" +#include "stringutils.h" + +namespace usvfs::log +{ + +enum class DisplayStyle : uint8_t +{ + Hex = 0x01 +}; + +class CallLoggerDummy +{ +public: + template + CallLoggerDummy& addParam(const char*, const T&, uint8_t style = 0) + { + return *this; + } +}; + +class CallLogger +{ +public: + explicit CallLogger(const char* function) + { + const char* namespaceend = strrchr(function, ':'); + + if (namespaceend != nullptr) { + function = namespaceend + 1; + } + + m_Message = function; + } + + ~CallLogger() + { + try { + static std::shared_ptr log = spdlog::get("hooks"); + log->debug("{}", m_Message); + } catch (...) { + // suppress all exceptions in destructor + } + } + + template + CallLogger& addParam(const char* name, const T& value, uint8_t style = 0); + +private: + std::string m_Message; +}; + +template +CallLogger& CallLogger::addParam(const char* name, const T& value, uint8_t style) +{ + static bool enabled = spdlog::get("hooks")->should_log(spdlog::level::debug); + typedef std::underlying_type::type DSType; + + if (enabled) { + if constexpr (std::is_pointer_v) { + if (value == nullptr) { + std::format_to(std::back_inserter(m_Message), "[{}=]", name); + } else { + std::format_to(std::back_inserter(m_Message), "[{}={}]", name, value); + } + } else if constexpr (std::is_integral_v) { + if (style & static_cast(DisplayStyle::Hex)) { + std::format_to(std::back_inserter(m_Message), "[{}={:x}]", name, value); + } else { + std::format_to(std::back_inserter(m_Message), "[{}={}]", name, value); + } + } else { + std::format_to(std::back_inserter(m_Message), "[{}={}]", name, value); + } + } + + return *this; +} + +spdlog::level::level_enum ConvertLogLevel(LogLevel level); +LogLevel ConvertLogLevel(spdlog::level::level_enum level); + +} // namespace usvfs::log + +// prefer the short variant of the function name, without signature. +// Fall back to the portable boost macro +#ifdef __FUNCTION__ +#define __MYFUNC__ __FUNCTION__ +#else +#define __MYFUNC__ BOOST_CURRENT_FUNCTION +#endif + +#define LOG_CALL() usvfs::log::CallLogger(__MYFUNC__) + +#define PARAM(val) addParam(#val, val) +#define PARAMHEX(val) \ + addParam(#val, val, static_cast(usvfs::log::DisplayStyle::Hex)) +#define PARAMWRAP(val) addParam(#val, usvfs::log::wrap(val)) diff --git a/libs/usvfs/src/shared/ntdll_declarations.cpp b/libs/usvfs/src/shared/ntdll_declarations.cpp new file mode 100644 index 0000000..b4bf0f5 --- /dev/null +++ b/libs/usvfs/src/shared/ntdll_declarations.cpp @@ -0,0 +1,75 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "ntdll_declarations.h" +#include + +#define LOAD_EXT(mod, name) \ + name = reinterpret_cast(::GetProcAddress(mod, #name)); \ + assert(name != nullptr) + +NtQueryDirectoryFile_type NtQueryDirectoryFile; +NtQueryDirectoryFileEx_type NtQueryDirectoryFileEx; +NtQueryFullAttributesFile_type NtQueryFullAttributesFile; +NtQueryAttributesFile_type NtQueryAttributesFile; +NtQueryObject_type NtQueryObject; +NtQueryInformationFile_type NtQueryInformationFile; +NtQueryInformationByName_type NtQueryInformationByName; +NtOpenFile_type NtOpenFile; +NtCreateFile_type NtCreateFile; +NtClose_type NtClose; +RtlDoesFileExists_U_type RtlDoesFileExists_U; +RtlDosPathNameToRelativeNtPathName_U_WithStatus_type + RtlDosPathNameToRelativeNtPathName_U_WithStatus; +RtlReleaseRelativeName_type RtlReleaseRelativeName; +RtlGetVersion_type RtlGetVersion; +NtTerminateProcess_type NtTerminateProcess; + +static bool ntdll_initialized; + +void ntdll_declarations_init() +{ + if (!ntdll_initialized) { + HMODULE ntDLLMod = GetModuleHandleW(L"ntdll.dll"); + + LOAD_EXT(ntDLLMod, NtQueryDirectoryFile); + LOAD_EXT(ntDLLMod, NtQueryDirectoryFileEx); + LOAD_EXT(ntDLLMod, NtQueryFullAttributesFile); + LOAD_EXT(ntDLLMod, NtQueryAttributesFile); + LOAD_EXT(ntDLLMod, NtQueryObject); + LOAD_EXT(ntDLLMod, NtQueryInformationFile); + LOAD_EXT(ntDLLMod, NtQueryInformationByName); + LOAD_EXT(ntDLLMod, NtCreateFile); + LOAD_EXT(ntDLLMod, NtOpenFile); + LOAD_EXT(ntDLLMod, NtClose); + LOAD_EXT(ntDLLMod, RtlDoesFileExists_U); + LOAD_EXT(ntDLLMod, RtlDosPathNameToRelativeNtPathName_U_WithStatus); + LOAD_EXT(ntDLLMod, RtlReleaseRelativeName); + LOAD_EXT(ntDLLMod, RtlGetVersion); + LOAD_EXT(ntDLLMod, NtTerminateProcess); + + ntdll_initialized = true; + } +} + +static struct __Initializer +{ + __Initializer() { ntdll_declarations_init(); } +} __initializer; diff --git a/libs/usvfs/src/shared/ntdll_declarations.h b/libs/usvfs/src/shared/ntdll_declarations.h new file mode 100644 index 0000000..813ad8c --- /dev/null +++ b/libs/usvfs/src/shared/ntdll_declarations.h @@ -0,0 +1,612 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "windows_sane.h" + +#pragma warning(push) +#pragma warning(disable : 4201) + +typedef LONG NTSTATUS; + +typedef struct _IO_STATUS_BLOCK +{ + union + { + NTSTATUS Status; + PVOID Pointer; + } DUMMYUNIONNAME; + + ULONG_PTR Information; +} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK; + +typedef struct _FILE_DIRECTORY_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + WCHAR FileName[1]; +} FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION; + +typedef struct _FILE_FULL_DIR_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + WCHAR FileName[1]; +} FILE_FULL_DIR_INFORMATION, *PFILE_FULL_DIR_INFORMATION; + +typedef struct _FILE_ID_FULL_DIR_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + LARGE_INTEGER FileId; + WCHAR FileName[1]; +} FILE_ID_FULL_DIR_INFORMATION, *PFILE_ID_FULL_DIR_INFORMATION; + +typedef struct _FILE_BOTH_DIR_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION; + +typedef struct _FILE_ID_BOTH_DIR_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + LARGE_INTEGER FileId; + WCHAR FileName[1]; +} FILE_ID_BOTH_DIR_INFORMATION, *PFILE_ID_BOTH_DIR_INFORMATION; + +typedef struct _FILE_BASIC_INFORMATION +{ + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + ULONG FileAttributes; +} FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION; + +typedef struct _FILE_RENAME_INFORMATION +{ +#if (_WIN32_WINNT >= _WIN32_WINNT_WIN10_RS1) + union + { + BOOLEAN ReplaceIfExists; // FileRenameInformation + ULONG Flags; // FileRenameInformationEx + } DUMMYUNIONNAME; +#else + BOOLEAN ReplaceIfExists; +#endif + HANDLE RootDirectory; + ULONG FileNameLength; + WCHAR FileName[1]; +} FILE_RENAME_INFORMATION, *PFILE_RENAME_INFORMATION; + +typedef struct _FILE_STANDARD_INFORMATION +{ + LARGE_INTEGER AllocationSize; + LARGE_INTEGER EndOfFile; + ULONG NumberOfLinks; + BOOLEAN DeletePending; + BOOLEAN Directory; +} FILE_STANDARD_INFORMATION, *PFILE_STANDARD_INFORMATION; + +typedef struct _FILE_NAMES_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + ULONG FileNameLength; + WCHAR FileName[1]; +} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION; + +typedef struct _FILE_INTERNAL_INFORMATION +{ + LARGE_INTEGER IndexNumber; +} FILE_INTERNAL_INFORMATION, *PFILE_INTERNAL_INFORMATION; + +typedef struct _FILE_EA_INFORMATION +{ + ULONG EaSize; +} FILE_EA_INFORMATION, *PFILE_EA_INFORMATION; + +typedef struct _FILE_ACCESS_INFORMATION +{ + ACCESS_MASK AccessFlags; +} FILE_ACCESS_INFORMATION, *PFILE_ACCESS_INFORMATION; + +typedef struct _FILE_POSITION_INFORMATION +{ + LARGE_INTEGER CurrentByteOffset; +} FILE_POSITION_INFORMATION, *PFILE_POSITION_INFORMATION; + +typedef struct _FILE_MODE_INFORMATION +{ + ULONG Mode; +} FILE_MODE_INFORMATION, *PFILE_MODE_INFORMATION; + +typedef struct _FILE_ALIGNMENT_INFORMATION +{ + ULONG AlignmentRequirement; +} FILE_ALIGNMENT_INFORMATION, *PFILE_ALIGNMENT_INFORMATION; + +typedef struct _FILE_NAME_INFORMATION +{ + ULONG FileNameLength; + WCHAR FileName[1]; +} FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION; + +typedef struct _FILE_ID_EXTD_DIR_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + ULONG ReparsePointTag; + FILE_ID_128 FileId; + WCHAR FileName[1]; +} FILE_ID_EXTD_DIR_INFORMATION, *PFILE_ID_EXTD_DIR_INFORMATION; + +typedef struct _FILE_ID_EXTD_BOTH_DIR_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + ULONG ReparsePointTag; + FILE_ID_128 FileId; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FILE_ID_EXTD_BOTH_DIR_INFORMATION, *PFILE_ID_EXTD_BOTH_DIR_INFORMATION; + +typedef struct _FILE_ID_64_EXTD_DIR_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + ULONG ReparsePointTag; + LARGE_INTEGER FileId; + WCHAR FileName[1]; +} FILE_ID_64_EXTD_DIR_INFORMATION, *PFILE_ID_64_EXTD_DIR_INFORMATION; + +typedef struct _FILE_ID_64_EXTD_BOTH_DIR_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + ULONG ReparsePointTag; + LARGE_INTEGER FileId; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FILE_ID_64_EXTD_BOTH_DIR_INFORMATION, *PFILE_ID_64_EXTD_BOTH_DIR_INFORMATION; + +typedef struct _FILE_ID_ALL_EXTD_DIR_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + ULONG ReparsePointTag; + LARGE_INTEGER FileId; + FILE_ID_128 FileId128; + WCHAR FileName[1]; +} FILE_ID_ALL_EXTD_DIR_INFORMATION, *PFILE_ID_ALL_EXTD_DIR_INFORMATION; + +typedef struct _FILE_ID_ALL_EXTD_BOTH_DIR_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + ULONG ReparsePointTag; + LARGE_INTEGER FileId; + FILE_ID_128 FileId128; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FILE_ID_ALL_EXTD_BOTH_DIR_INFORMATION, *PFILE_ID_ALL_EXTD_BOTH_DIR_INFORMATION; + +typedef struct _FILE_ALL_INFORMATION +{ + FILE_BASIC_INFORMATION BasicInformation; + FILE_STANDARD_INFORMATION StandardInformation; + FILE_INTERNAL_INFORMATION InternalInformation; + FILE_EA_INFORMATION EaInformation; + FILE_ACCESS_INFORMATION AccessInformation; + FILE_POSITION_INFORMATION PositionInformation; + FILE_MODE_INFORMATION ModeInformation; + FILE_ALIGNMENT_INFORMATION AlignmentInformation; + FILE_NAME_INFORMATION NameInformation; +} FILE_ALL_INFORMATION, *PFILE_ALL_INFORMATION; + +typedef struct _FILE_OBJECTID_INFORMATION +{ + LONGLONG FileReference; + UCHAR ObjectId[16]; + union + { + struct + { + UCHAR BirthVolumeId[16]; + UCHAR BirthObjectId[16]; + UCHAR DomainId[16]; + }; + UCHAR ExtendedInfo[48]; + }; +} FILE_OBJECTID_INFORMATION, *PFILE_OBJECTID_INFORMATION; + +typedef struct _FILE_REPARSE_POINT_INFORMATION +{ + LONGLONG FileReference; + ULONG Tag; +} FILE_REPARSE_POINT_INFORMATION, *PFILE_REPARSE_POINT_INFORMATION; + +// copied from ntstatus.h +#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) +#define STATUS_BUFFER_OVERFLOW ((NTSTATUS)0x80000005L) +#define STATUS_NO_MORE_FILES ((NTSTATUS)0x80000006L) +#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) +#define STATUS_NO_SUCH_FILE ((NTSTATUS)0xC000000FL) + +#define SL_RESTART_SCAN 0x01 +#define SL_RETURN_SINGLE_ENTRY 0x02 +#define SL_INDEX_SPECIFIED 0x04 +#define SL_RETURN_ON_DISK_ENTRIES_ONLY 0x08 + +#define SL_QUERY_DIRECTORY_MASK 0x0b + +typedef enum _FILE_INFORMATION_CLASS +{ + FileDirectoryInformation = 1, + FileFullDirectoryInformation = 2, + FileBothDirectoryInformation = 3, + FileStandardInformation = 5, + FileNameInformation = 9, + FileRenameInformation = 10, + FileNamesInformation = 12, + FileAllInformation = 18, + FileObjectIdInformation = 29, + FileReparsePointInformation = 33, + FileIdBothDirectoryInformation = 37, + FileIdFullDirectoryInformation = 38, + FileNormalizedNameInformation = 48, + FileIdExtdDirectoryInformation = 60, + FileIdExtdBothDirectoryInformation = 63, + FileId64ExtdDirectoryInformation = 78, + FileId64ExtdBothDirectoryInformation = 79, + FileIdAllExtdDirectoryInformation = 80, + FileIdAllExtdBothDirectoryInformation = 81 +} FILE_INFORMATION_CLASS, + *PFILE_INFORMATION_CLASS; + +typedef enum _MODE +{ + KernelMode, + UserMode, + MaximumMode +} MODE; + +typedef struct _IO_STATUS_BLOCK IO_STATUS_BLOCK; + +typedef struct _IO_STATUS_BLOCK* PIO_STATUS_BLOCK; +// typedef VOID (NTAPI *PIO_APC_ROUTINE )(__in PVOID ApcContext, __in +// PIO_STATUS_BLOCK IoStatusBlock, __in ULONG Reserved); +typedef VOID(NTAPI* PIO_APC_ROUTINE)(PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, + ULONG Reserved); +typedef enum _FILE_INFORMATION_CLASS FILE_INFORMATION_CLASS; + +typedef struct _UNICODE_STRING +{ + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; +} UNICODE_STRING, *PUNICODE_STRING; +typedef const UNICODE_STRING* PCUNICODE_STRING; + +typedef struct _OBJECT_ATTRIBUTES +{ + ULONG Length; + HANDLE RootDirectory; + PUNICODE_STRING ObjectName; + ULONG Attributes; + PVOID SecurityDescriptor; + PVOID SecurityQualityOfService; +} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES; + +typedef enum _POOL_TYPE +{ + NonPagedPool, + NonPagedPoolExecute = NonPagedPool, + PagedPool, + NonPagedPoolMustSucceed = NonPagedPool + 2, + DontUseThisType, + NonPagedPoolCacheAligned = NonPagedPool + 4, + PagedPoolCacheAligned, + NonPagedPoolCacheAlignedMustS = NonPagedPool + 6, + MaxPoolType, + NonPagedPoolBase = 0, + NonPagedPoolBaseMustSucceed = NonPagedPoolBase + 2, + NonPagedPoolBaseCacheAligned = NonPagedPoolBase + 4, + NonPagedPoolBaseCacheAlignedMustS = NonPagedPoolBase + 6, + NonPagedPoolSession = 32, + PagedPoolSession = NonPagedPoolSession + 1, + NonPagedPoolMustSucceedSession = PagedPoolSession + 1, + DontUseThisTypeSession = NonPagedPoolMustSucceedSession + 1, + NonPagedPoolCacheAlignedSession = DontUseThisTypeSession + 1, + PagedPoolCacheAlignedSession = NonPagedPoolCacheAlignedSession + 1, + NonPagedPoolCacheAlignedMustSSession = PagedPoolCacheAlignedSession + 1, + NonPagedPoolNx = 512, + NonPagedPoolNxCacheAligned = NonPagedPoolNx + 4, + NonPagedPoolSessionNx = NonPagedPoolNx + 32 +} POOL_TYPE; + +typedef struct _OBJECT_TYPE_INITIALIZER +{ + WORD Length; + UCHAR ObjectTypeFlags; + ULONG CaseInsensitive : 1; + ULONG UnnamedObjectsOnly : 1; + ULONG UseDefaultObject : 1; + ULONG SecurityRequired : 1; + ULONG MaintainHandleCount : 1; + ULONG MaintainTypeList : 1; + ULONG ObjectTypeCode; + ULONG InvalidAttributes; + GENERIC_MAPPING GenericMapping; + ULONG ValidAccessMask; + POOL_TYPE PoolType; + ULONG DefaultPagedPoolCharge; + ULONG DefaultNonPagedPoolCharge; + PVOID DumpProcedure; + LONG* OpenProcedure; + PVOID CloseProcedure; + PVOID DeleteProcedure; + LONG* ParseProcedure; + LONG* SecurityProcedure; + LONG* QueryNameProcedure; + UCHAR* OkayToCloseProcedure; +} OBJECT_TYPE_INITIALIZER, *POBJECT_TYPE_INITIALIZER; + +typedef CCHAR KPROCESSOR_MODE; + +typedef struct _OBJECT_HANDLE_INFORMATION +{ + ULONG HandleAttributes; + ACCESS_MASK GrantedAccess; +} OBJECT_HANDLE_INFORMATION, *POBJECT_HANDLE_INFORMATION; + +typedef struct _OBJECT_NAME_INFORMATION +{ + UNICODE_STRING Name; +} OBJECT_NAME_INFORMATION, *POBJECT_NAME_INFORMATION; + +typedef enum _OBJECT_INFORMATION_CLASS +{ + ObjectBasicInformation = 0, + ObjectNameInformation = 1, + ObjectTypeInformation = 2 +} OBJECT_INFORMATION_CLASS; + +typedef struct _RTL_RELATIVE_NAME +{ + UNICODE_STRING RelativeName; + HANDLE ContainingDirectory; + void* CurDirRef; +} RTL_RELATIVE_NAME, *PRTL_RELATIVE_NAME; + +typedef struct _FILE_NETWORK_OPEN_INFORMATION +{ + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER AllocationSize; + LARGE_INTEGER EndOfFile; + ULONG FileAttributes; +} FILE_NETWORK_OPEN_INFORMATION, *PFILE_NETWORK_OPEN_INFORMATION; + +#define FILE_DIRECTORY_FILE 0x00000001 +#define FILE_WRITE_THROUGH 0x00000002 +#define FILE_SEQUENTIAL_ONLY 0x00000004 +#define FILE_NO_INTERMEDIATE_BUFFERING 0x00000008 +#define FILE_SYNCHRONOUS_IO_ALERT 0x00000010 +#define FILE_SYNCHRONOUS_IO_NONALERT 0x00000020 +#define FILE_NON_DIRECTORY_FILE 0x00000040 +#define FILE_CREATE_TREE_CONNECTION 0x00000080 +#define FILE_COMPLETE_IF_OPLOCKED 0x00000100 +#define FILE_NO_EA_KNOWLEDGE 0x00000200 +#define FILE_OPEN_REMOTE_INSTANCE 0x00000400 +#define FILE_RANDOM_ACCESS 0x00000800 +#define FILE_DELETE_ON_CLOSE 0x00001000 +#define FILE_OPEN_BY_FILE_ID 0x00002000 +#define FILE_OPEN_FOR_BACKUP_INTENT 0x00004000 +#define FILE_NO_COMPRESSION 0x00008000 +#define FILE_OPEN_REQUIRING_OPLOCK 0x00010000 +#define FILE_DISALLOW_EXCLUSIVE 0x00020000 +#define FILE_SESSION_AWARE 0x00040000 +#define FILE_RESERVE_OPFILTER 0x00100000 +#define FILE_OPEN_REPARSE_POINT 0x00200000 +#define FILE_OPEN_NO_RECALL 0x00400000 +#define FILE_OPEN_FOR_FREE_SPACE_QUERY 0x00800000 +#define FILE_CONTAINS_EXTENDED_CREATE_INFORMATION 0x10000000 + +// Nt + +using NtQueryDirectoryFile_type = NTSTATUS(WINAPI*)(HANDLE, HANDLE, PIO_APC_ROUTINE, + PVOID, PIO_STATUS_BLOCK, PVOID, + ULONG, FILE_INFORMATION_CLASS, + BOOLEAN, PUNICODE_STRING, BOOLEAN); +using NtQueryDirectoryFileEx_type = NTSTATUS(WINAPI*)(HANDLE, HANDLE, PIO_APC_ROUTINE, + PVOID, PIO_STATUS_BLOCK, PVOID, + ULONG, FILE_INFORMATION_CLASS, + ULONG, PUNICODE_STRING); + +using NtQueryFullAttributesFile_type = + NTSTATUS(WINAPI*)(POBJECT_ATTRIBUTES, PFILE_NETWORK_OPEN_INFORMATION); +using NtQueryAttributesFile_type = NTSTATUS(WINAPI*)(POBJECT_ATTRIBUTES, + PFILE_BASIC_INFORMATION); + +using NtQueryObject_type = NTSTATUS(WINAPI*)( + HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass, + PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength); +using NtQueryInformationFile_type = NTSTATUS(WINAPI*)( + HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, + ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); + +using NtQueryInformationByName_type = NTSTATUS(WINAPI*)( + HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, + ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); + +using NtOpenFile_type = NTSTATUS(WINAPI*)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, + PIO_STATUS_BLOCK, ULONG, ULONG); +using NtCreateFile_type = NTSTATUS(WINAPI*)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, + PIO_STATUS_BLOCK, PLARGE_INTEGER, ULONG, + ULONG, ULONG, ULONG, PVOID, ULONG); + +using NtClose_type = NTSTATUS(WINAPI*)(HANDLE); + +using NtTerminateProcess_type = NTSTATUS(WINAPI*)(HANDLE ProcessHandle, + NTSTATUS ExitStatus); + +// Rtl + +using RtlDoesFileExists_U_type = NTSYSAPI BOOLEAN(NTAPI*)(PCWSTR); +using RtlDosPathNameToRelativeNtPathName_U_WithStatus_type = + NTSTATUS(NTAPI*)(PCWSTR DosFileName, PUNICODE_STRING NtFileName, PWSTR* FilePath, + PRTL_RELATIVE_NAME RelativeName); +using RtlReleaseRelativeName_type = void(NTAPI*)(PRTL_RELATIVE_NAME RelativeName); +using RtlGetVersion_type = NTSTATUS(NTAPI*)(PRTL_OSVERSIONINFOW); + +extern NtQueryDirectoryFile_type NtQueryDirectoryFile; +extern NtQueryDirectoryFileEx_type NtQueryDirectoryFileEx; +extern NtQueryFullAttributesFile_type NtQueryFullAttributesFile; +extern NtQueryAttributesFile_type NtQueryAttributesFile; +extern NtQueryObject_type NtQueryObject; +extern NtQueryInformationFile_type NtQueryInformationFile; +extern NtQueryInformationByName_type NtQueryInformationByName; +extern NtOpenFile_type NtOpenFile; +extern NtCreateFile_type NtCreateFile; +extern NtClose_type NtClose; +extern NtTerminateProcess_type NtTerminateProcess; +extern RtlDoesFileExists_U_type RtlDoesFileExists_U; +extern RtlDosPathNameToRelativeNtPathName_U_WithStatus_type + RtlDosPathNameToRelativeNtPathName_U_WithStatus; +extern RtlReleaseRelativeName_type RtlReleaseRelativeName; +extern RtlGetVersion_type RtlGetVersion; + +// ensures ntdll functions have been initialized (only needed during static objects +// initialization) +void ntdll_declarations_init(); + +#pragma warning(pop) diff --git a/libs/usvfs/src/shared/pch.cpp b/libs/usvfs/src/shared/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/libs/usvfs/src/shared/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/libs/usvfs/src/shared/pch.h b/libs/usvfs/src/shared/pch.h new file mode 100644 index 0000000..e787f68 --- /dev/null +++ b/libs/usvfs/src/shared/pch.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +// clang-format off +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +// clang-format on + +#define BOOST_INTERPROCESS_SEGMENT_MANAGER_ABI 1 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace fs = boost::filesystem; diff --git a/libs/usvfs/src/shared/shared_memory.h b/libs/usvfs/src/shared/shared_memory.h new file mode 100644 index 0000000..28da34b --- /dev/null +++ b/libs/usvfs/src/shared/shared_memory.h @@ -0,0 +1,53 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ + +#pragma once + +namespace bi = boost::interprocess; +namespace bc = boost::container; + +namespace usvfs::shared +{ + +template +using OffsetPtrT = bi::offset_ptr; + +using VoidPointerT = OffsetPtrT; + +// important: the windows shared memory mechanism, unlike other implementations, +// automatically removes the SHM object when there are no more "subscribers". +// MO currently depends on that feature! + +// managed_windows_shared_memory apparently doesn't support sharing between +// 64bit and 32bit processes +using managed_windows_shared_memory = bi::basic_managed_windows_shared_memory< + char, bi::rbtree_best_fit, bi::iset_index>; + +using SharedMemoryT = managed_windows_shared_memory; +using SegmentManagerT = SharedMemoryT::segment_manager; + +using VoidAllocatorT = boost::container::scoped_allocator_adaptor< + boost::interprocess::allocator>; +using CharAllocatorT = VoidAllocatorT::rebind::other; + +using StringT = bc::basic_string, CharAllocatorT>; + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/shmlogger.cpp b/libs/usvfs/src/shared/shmlogger.cpp new file mode 100644 index 0000000..66512de --- /dev/null +++ b/libs/usvfs/src/shared/shmlogger.cpp @@ -0,0 +1,205 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma warning(disable : 4714) +#pragma warning(disable : 4503) +#pragma warning(push, 3) +#include "shmlogger.h" +#pragma warning(pop) + +#pragma warning(disable : 4996) + +using namespace boost::interprocess; +using namespace boost::posix_time; + +SHMLogger* SHMLogger::s_Instance = nullptr; + +SHMLogger::owner_t SHMLogger::owner; +SHMLogger::client_t SHMLogger::client; + +SHMLogger::SHMLogger(owner_t, const std::string& queueName) + : m_QueueName(queueName), + m_LogQueue(create_only, queueName.c_str(), MESSAGE_COUNT, MESSAGE_SIZE), + m_DroppedMessages(0) +{ + if (s_Instance != nullptr) { + throw std::runtime_error("duplicate shm logger instantiation"); + } else { + s_Instance = this; + } +} + +SHMLogger::SHMLogger(client_t, const std::string& queueName) + : m_QueueName(queueName), m_LogQueue(open_only, queueName.c_str()), + m_DroppedMessages(0) +{ + if (s_Instance != nullptr) { + throw std::runtime_error("duplicate shm logger instantiation"); + } else { + s_Instance = this; + } +} + +SHMLogger::~SHMLogger() +{ + s_Instance = nullptr; + message_queue_interop::remove(m_QueueName.c_str()); +} + +SHMLogger& SHMLogger::create(const char* instanceName) +{ + if (s_Instance != nullptr) { + throw std::runtime_error("duplicate shm logger instantiation"); + } else { + std::string queueName = std::string("__shm_sink_") + instanceName; + message_queue::remove(queueName.c_str()); + new SHMLogger(owner, queueName); + atexit([]() { + delete s_Instance; + }); + } + return *s_Instance; +} + +SHMLogger& SHMLogger::open(const char* instanceName) +{ + if (s_Instance != nullptr) { + throw std::runtime_error("duplicate shm logger instantiation"); + } else { + new SHMLogger(client, std::string("__shm_sink_") + instanceName); + } + return *s_Instance; +} + +void SHMLogger::free() +{ + if (s_Instance != nullptr) { + SHMLogger* temp = s_Instance; + s_Instance = nullptr; + delete temp; + } +} + +bool SHMLogger::tryGet(char* buffer, size_t bufferSize) +{ + message_queue_interop::size_type receivedSize; + unsigned int prio; + bool res = m_LogQueue.try_receive(buffer, static_cast(bufferSize), + receivedSize, prio); + if (res) { + buffer[std::min(bufferSize - 1, static_cast(receivedSize))] = '\0'; + } + return res; +} + +void SHMLogger::get(char* buffer, size_t bufferSize) +{ + message_queue_interop::size_type receivedSize; + unsigned int prio; + m_LogQueue.receive(buffer, static_cast(bufferSize), receivedSize, prio); + buffer[std::min(bufferSize - 1, static_cast(receivedSize))] = '\0'; +} + +usvfs::sinks::shm_sink::shm_sink(const char* queueName) + : m_LogQueue(open_only, (std::string("__shm_sink_") + queueName).c_str()), + m_DroppedMessages(0) +{} + +void usvfs::sinks::shm_sink::flush_() {} + +void usvfs::sinks::shm_sink::sink_it_(const spdlog::details::log_msg& msg) +{ + int droppedMessages = m_DroppedMessages.load(std::memory_order_relaxed); + if (droppedMessages > 0) { + auto dropMessage = std::format("{} debug messages dropped", droppedMessages); + if (m_LogQueue.try_send(dropMessage.c_str(), + static_cast(dropMessage.size()), 0)) { + m_DroppedMessages.store(0, std::memory_order_relaxed); + } + } + + std::string message{msg.payload}; + + // blacklist %USERNAME% because PII + static const std::string username = std::string(getenv("USERNAME")); + boost::algorithm::ireplace_all(message, std::string("\\") + username, "\\USERNAME"); + boost::algorithm::ireplace_all(message, std::string("/") + username, "/USERNAME"); + + if (message.length() > SHMLogger::MESSAGE_SIZE) { + std::vector splitVec; + boost::split(splitVec, message, boost::is_any_of("\r\n"), boost::token_compress_on); + for (const std::string& line : splitVec) { + output(msg.level, line); + } + } else { + output(msg.level, message); + } +} + +void usvfs::sinks::shm_sink::output(spdlog::level::level_enum lev, + const std::string& message) +{ + bool sent = true; + + // spdlog auto-append line breaks which we don't need. Probably would be + // better to not write the breaks to begin with? + size_t count = std::min(message.find_last_not_of("\r\n") + 1, + static_cast(m_LogQueue.get_max_msg_size())); + + // depending on the log level, drop less important messages if the receiver + // can't keep up + switch (lev) { + case spdlog::level::trace: + case spdlog::level::debug: + case spdlog::level::info: { + // m_LogQueue.send(message.c_str(), count, 0); + sent = m_LogQueue.try_send(message.c_str(), static_cast(count), 0); + } break; + case spdlog::level::err: + case spdlog::level::critical: { + m_LogQueue.send(message.c_str(), static_cast(count), 0); + } break; + default: { + boost::posix_time::ptime time = + microsec_clock::universal_time() + boost::posix_time::milliseconds(200); + sent = m_LogQueue.timed_send(message.c_str(), static_cast(count), 0, + time); + } break; + } + + if (!sent) { + m_DroppedMessages.fetch_add(1, std::memory_order_relaxed); + } +} + +void __cdecl boost::interprocess::ipcdetail::get_shared_dir(std::string& shared_dir) +{ + PWSTR path; + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_ProgramData, 0, NULL, &path))) { + _bstr_t bPath(path); + shared_dir = (char*)bPath; + shared_dir += "\\USVFS"; + } else { + shared_dir = "C:\\ProgramData\\USVFS"; + } + boost::filesystem::path boostPath(shared_dir); + if (!boost::filesystem::exists(boostPath)) + boost::filesystem::create_directories(boostPath); +} diff --git a/libs/usvfs/src/shared/shmlogger.h b/libs/usvfs/src/shared/shmlogger.h new file mode 100644 index 0000000..158f941 --- /dev/null +++ b/libs/usvfs/src/shared/shmlogger.h @@ -0,0 +1,105 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include +#include + +#include "logging.h" +#include "shared_memory.h" +#include "windows_sane.h" + +typedef boost::interprocess::message_queue_t + message_queue_interop; + +namespace usvfs::sinks +{ +class shm_sink : public spdlog::sinks::base_sink +{ +public: + shm_sink(const char* queueName); + +protected: + void sink_it_(const spdlog::details::log_msg& msg) override; + void output(spdlog::level::level_enum lev, const std::string& message); + void flush_() override; + +private: + message_queue_interop m_LogQueue; + std::atomic m_DroppedMessages; +}; + +} // namespace usvfs::sinks + +class SHMLogger +{ +public: + static const size_t MESSAGE_COUNT = 1024; + static const size_t MESSAGE_SIZE = 512; + + static SHMLogger& create(const char* instanceName); + static SHMLogger& open(const char* instanceName); + static void free(); + + static bool isInstantiated() { return s_Instance != nullptr; } + + static inline SHMLogger& instance() + { + if (s_Instance == nullptr) { + throw std::runtime_error("shm logger not instantiated"); + } + + return *s_Instance; + } + + void log(LogLevel logLevel, const std::string& message); + bool tryGet(char* buffer, size_t bufferSize); + void get(char* buffer, size_t bufferSize); + +private: + struct owner_t + {}; + static owner_t owner; + + struct client_t + {}; + static client_t client; + +private: + SHMLogger(owner_t, const std::string& instanceName); + SHMLogger(client_t, const std::string& instanceName); + + SHMLogger(const SHMLogger&) = delete; + SHMLogger& operator=(const SHMLogger&) = delete; + + ~SHMLogger(); + +private: + static SHMLogger* s_Instance; + + message_queue_interop m_LogQueue; + + std::string m_SHMName; + std::string m_LockName; + std::string m_QueueName; + + std::atomic m_DroppedMessages; +}; diff --git a/libs/usvfs/src/shared/stringcast.cpp b/libs/usvfs/src/shared/stringcast.cpp new file mode 100644 index 0000000..47822d5 --- /dev/null +++ b/libs/usvfs/src/shared/stringcast.cpp @@ -0,0 +1,40 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "stringcast.h" + +namespace usvfs::shared +{ + +UINT windowsCP(CodePage codePage) +{ + switch (codePage) { + case CodePage::LOCAL: + return CP_ACP; + case CodePage::UTF8: + return CP_UTF8; + case CodePage::LATIN1: + return 850; + } + // this should not be possible in practice + throw std::runtime_error("unsupported codePage"); +} + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/stringcast.h b/libs/usvfs/src/shared/stringcast.h new file mode 100644 index 0000000..0ce56d1 --- /dev/null +++ b/libs/usvfs/src/shared/stringcast.h @@ -0,0 +1,170 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "exceptionex.h" + +namespace usvfs::shared +{ + +enum class CodePage +{ + LOCAL, + LATIN1, + UTF8 +}; + +template +class string_cast_impl +{ +public: + static ToT cast(const FromT& source, CodePage codePage, size_t sourceLength); +}; + +template +ToT string_cast(FromT source, CodePage codePage = CodePage::LOCAL, + size_t sourceLength = std::numeric_limits::max()) +{ + return string_cast_impl::cast(source, codePage, sourceLength); +} + +template +class string_cast_impl> +{ +public: + static ToT cast(const std::basic_string& source, CodePage codePage, + size_t sourceLength) + { + return string_cast_impl::cast(source.c_str(), codePage, + sourceLength); + } +}; + +template +class string_cast_impl +{ + BOOST_STATIC_ASSERT(!boost::is_base_and_derived::value); + +public: + static ToT cast(CharT* source, CodePage codePage, size_t sourceLength) + { + return string_cast_impl::cast(source, codePage, sourceLength); + } +}; + +template +class string_cast_impl +{ +public: + static ToT cast(CharT (&source)[N], CodePage codePage, size_t sourceLength) + { + return string_cast_impl::cast(source, codePage, sourceLength); + } +}; + +UINT windowsCP(CodePage codePage); + +template <> +class string_cast_impl +{ +public: + static std::string cast(const wchar_t* const& source, CodePage codePage, + size_t sourceLength) + { + std::string result; + + if (sourceLength == std::numeric_limits::max()) { + sourceLength = wcslen(source); + } + + if (sourceLength > 0) { + // use utf8 or local 8-bit encoding depending on user choice + UINT cp = windowsCP(codePage); + // preflight to find out the required buffer size + int outLength = WideCharToMultiByte(cp, 0, source, static_cast(sourceLength), + nullptr, 0, nullptr, nullptr); + if (outLength == 0) { + throw windows_error("string conversion failed"); + } + result.resize(outLength); + outLength = WideCharToMultiByte(cp, 0, source, static_cast(sourceLength), + &result[0], outLength, nullptr, nullptr); + if (outLength == 0) { + throw windows_error("string conversion failed"); + } + // fix output string length (i.e. in case of unconvertible characters + while (result[outLength - 1] == L'\0') { + result.resize(--outLength); + } + } + + return result; + } +}; + +template <> +class string_cast_impl +{ +public: + static std::wstring cast(const char* const& source, CodePage codePage, + size_t sourceLength) + { + std::wstring result; + + if (sourceLength == std::numeric_limits::max()) { + sourceLength = strlen(source); + } + + if (sourceLength > 0) { + // use utf8 or local 8-bit encoding depending on user choice + UINT cp = windowsCP(codePage); + // preflight to find out the required source size + int outLength = MultiByteToWideChar(cp, 0, source, static_cast(sourceLength), + &result[0], 0); + if (outLength == 0) { + throw windows_error("string conversion failed"); + } + result.resize(outLength); + outLength = MultiByteToWideChar(cp, 0, source, static_cast(sourceLength), + &result[0], outLength); + if (outLength == 0) { + throw windows_error("string conversion failed"); + } + while (result[outLength - 1] == L'\0') { + result.resize(--outLength); + } + } + + return result; + } +}; + +template <> +class string_cast_impl +{ +public: + static std::wstring cast(const wchar_t* const& source, CodePage, size_t) + { + return std::wstring(source); + } +}; + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/stringutils.cpp b/libs/usvfs/src/shared/stringutils.cpp new file mode 100644 index 0000000..2ddbf64 --- /dev/null +++ b/libs/usvfs/src/shared/stringutils.cpp @@ -0,0 +1,146 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "stringutils.h" +#include "windows_sane.h" + +namespace usvfs::shared +{ + +void strncpy_sz(char* dest, const char* src, size_t destSize) +{ + if (destSize > 0) { + strncpy(dest, src, destSize - 1); + dest[destSize - 1] = '\0'; + } +} + +void wcsncpy_sz(wchar_t* dest, const wchar_t* src, size_t destSize) +{ + if ((destSize > 0) && (dest != nullptr)) { + wcsncpy(dest, src, destSize - 1); + dest[destSize - 1] = L'\0'; + } +} + +bool startswith(const wchar_t* string, const wchar_t* subString) +{ + while ((*string != '\0') && (*subString != '\0')) { + if (towlower(*string) != towlower(*subString)) { + return false; + } + ++string; + ++subString; + } + + return *subString == '\0'; +} + +static fs::path normalize(const fs::path& path) +{ + fs::path result; + + boost::locale::generator gen; + auto loc = gen("en_US.UTF-8"); + for (fs::path::iterator iter = path.begin(); iter != path.end(); ++iter) { + if (*iter == "..") { + result = result.parent_path(); + } else if (*iter != ".") { + result /= boost::to_lower_copy(iter->string(), loc); + } // single dot is ignored + } + return result; +} + +fs::path make_relative(const fs::path& fromIn, const fs::path& toIn) +{ + // converting path to lower case to make iterator comparison work correctly + // on case-insenstive filesystems + fs::path from(fs::absolute(fromIn)); + fs::path to(fs::absolute(toIn)); + + // find common base + fs::path::const_iterator fromIter(from.begin()); + fs::path::const_iterator toIter(to.begin()); + + // TODO the following equivalent test is probably quite expensive as new + // paths are created for each iteration but the case sensitivity depends on + // the fs + while ((fromIter != from.end()) && (toIter != to.end()) && + (boost::iequals(fromIter->string(), toIter->string()))) { + ++fromIter; + ++toIter; + } + + // Navigate backwards in directory to reach previously found base + fs::path result; + for (; fromIter != from.end(); ++fromIter) { + if (*fromIter != ".") { + result /= ".."; + } + } + + // Now navigate down the directory branch + for (; toIter != to.end(); ++toIter) { + result /= *toIter; + } + return result; +} + +std::string to_hex(void* bufferIn, size_t bufferSize) +{ + unsigned char* buffer = static_cast(bufferIn); + std::ostringstream temp; + temp << std::hex; + for (size_t i = 0; i < bufferSize; ++i) { + temp << std::setfill('0') << std::setw(2) << (unsigned int)buffer[i]; + if ((i % 16) == 15) { + temp << "\n"; + } else { + temp << " "; + } + } + return temp.str(); +} + +std::wstring to_upper(const std::wstring& input) +{ + std::wstring result; + result.resize(input.size()); + ::LCMapStringW(LOCALE_INVARIANT, LCMAP_UPPERCASE, input.c_str(), + static_cast(input.size()), &result[0], + static_cast(result.size())); + return result; +} + +std::string byte_string(std::size_t n) +{ + auto s = std::to_string(n); + auto p = s.size(); + + while (p > 3) { + p -= 3; + s.insert(p, 1, ','); + } + + return s + " B"; +} + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/stringutils.h b/libs/usvfs/src/shared/stringutils.h new file mode 100644 index 0000000..64d0af7 --- /dev/null +++ b/libs/usvfs/src/shared/stringutils.h @@ -0,0 +1,57 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +namespace usvfs::shared +{ + +void strncpy_sz(char* dest, const char* src, size_t destSize); +void wcsncpy_sz(wchar_t* dest, const wchar_t* src, size_t destSize); + +bool startswith(const wchar_t* string, const wchar_t* subString); + +// Return path when appended to a_From will resolve to same as a_To +fs::path make_relative(const fs::path& from, const fs::path& to); + +std::string to_hex(void* bufferIn, size_t bufferSize); + +// convert unicode string to upper-case (locale invariant) +std::wstring to_upper(const std::wstring& input); + +// formats a number with thousand separators and B at the end +// +std::string byte_string(std::size_t n); + +class FormatGuard +{ + std::ostream& m_Stream; + std::ios::fmtflags m_Flags; + +public: + FormatGuard(std::ostream& stream) : m_Stream(stream), m_Flags(stream.flags()) {} + + ~FormatGuard() { m_Stream.flags(m_Flags); } + + FormatGuard(const FormatGuard&) = delete; + FormatGuard& operator=(FormatGuard&) = delete; +}; + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/tree_container.h b/libs/usvfs/src/shared/tree_container.h new file mode 100644 index 0000000..a2f61fa --- /dev/null +++ b/libs/usvfs/src/shared/tree_container.h @@ -0,0 +1,630 @@ +#pragma once + +#include "directory_tree.h" +#include "shared_memory.h" + +namespace usvfs::shared +{ + +// smart pointer to DirectoryTrees (only intended for top-level nodes). This +// will transparently switch to new shared memory regions in case they get +// reallocated +// +// +// reassign() is called from a variety of places when the current chunk of +// shared memory is full or is marked as being outdated; its job is to either +// find another chunk that may have been created by another process or to create +// a brand new one +// +// +// if there is only a single process hooked, it will slowly fill up the shared +// memory when adding files, eventually throw a bi::bad_alloc and end up in +// reassign(); a new block will be allocated, the data copied over, and the old +// block will be deallocated +// +// +// when multiple processes are involved, things are more complicated +// +// two processes A and B will start by using the same shared memory, but they +// have their own pointer to it that's local to the process (the `m_TreeMeta` +// member variable) +// +// so when process A fills up the shared memory and reallocates it, process B +// is still pointing to the old shared memory; only when process B does some +// operation that accesses the file tree will the pointer be checked and +// adjusted to point to the new shared memory +// +// in this example, when process A ran out of memory, it set `outdated` to +// `true` in the shared memory block, allocated a new one and copied the data +// over, but it did not deallocate the block because process B is still +// pointing to it +// +// when process B tries to access the block, it checks `outdated` (see get()); +// if it's true, it means that it's pointing to an outdated shared memory block +// and must find the new one that process A created +// +// the new block is not necessarily the very next name, it is possible for +// process A to burn through a series of blocks quickly when adding a bunch of +// new files, and these blocks will all have been deallocated by the time +// process B tries to find the newest one +// +// so when a process sees its block as outdated, it will try to open a bunch of +// names until one exists that is not outdated; if it can't find the block +// (shouldn't happen), it will just create a new one +// +template +class TreeContainer +{ +public: + /** + * @brief Constructor + * @param SHMName name of the shared memory holding the tree. This should contain the + * running number + * @param size initial size in bytes of the container. since the tree is resized by + * doubling this should be a power of two. 64k is supposed to be the page size on + * windows so smaller allocations make little sense + * @note size can't be too small. If initial allocations fail automatic growing won't + * work + */ + TreeContainer(const std::string& SHMName, size_t size = 64 * 1024) + : m_TreeMeta(nullptr), m_SHMName(SHMName) + { + std::locale global_loc = std::locale(); + std::locale loc(global_loc, new fs::detail::utf8_codecvt_facet); + fs::path::imbue(loc); + + // append _1 to the name if it doesn't end with _N already + std::regex pattern(R"exp((.*_)(\d+))exp"); + std::smatch match; + std::string shmName(m_SHMName.c_str()); + regex_match(shmName, match, pattern); + + if (match.size() != 3) { + m_SHMName += "_1"; + } + + // creates a new memory block if this is the first process to run or attach + // to an already existing one + createOrOpen(m_SHMName, size); + + spdlog::get("usvfs")->info("attached to {0} with {1} nodes, size {2}", m_SHMName, + m_TreeMeta->tree->numNodesRecursive(), + byte_string(m_SHM->get_size())); + } + + TreeContainer(const TreeContainer&) = delete; + TreeContainer& operator=(const TreeContainer&) = delete; + + ~TreeContainer() + { + if (unassign(m_SHM, m_TreeMeta)) { + bi::shared_memory_object::remove(m_SHMName.c_str()); + } + } + + /** + * @return retrieve an allocator that can be used to create objects in this tree + */ + VoidAllocatorT allocator() { return VoidAllocatorT(m_SHM->get_segment_manager()); } + + template + typename TreeT::DataT create(Arguments&&... args) + { + return TreeT::DataT(std::forward(args)..., allocator()); + } + + TreeT* operator->() { return get(); } + + /** + * @return raw pointer to the managed tree + */ + TreeT* get() + { + if (m_TreeMeta->outdated) { + reassign(); + } + + return m_TreeMeta->tree.get(); + } + + /** + * @return raw const pointer to the managed tree + */ + const TreeT* get() const + { + if (m_TreeMeta->outdated) { + // safe const_cast, TreeContainer are never created const + const_cast*>(this)->reassign(); + } + + return m_TreeMeta->tree.get(); + } + + const TreeT* operator->() const { return get(); } + + /** + * @return current name of the managed shared memory + */ + std::string shmName() const { return m_SHMName; } + + void clear() { m_TreeMeta->tree->clear(); } + + /** + * @brief add a new file to the tree + * + * @param name name of the file, expected to be relative to this directory + * @param data the file data to attach + * @param flags flags for this files + * @param overwrite if true, the new leaf will overwrite an existing one that compares + *as "equal" + * @return pointer to the new node or a null ptr + **/ + template + typename TreeT::NodePtrT addFile(const fs::path& name, const T& data, + TreeFlags flags = 0, bool overwrite = true) + { + for (;;) { + DecomposablePath dp(name.string()); + + try { + return addNode(m_TreeMeta->tree.get(), dp, data, overwrite, flags, allocator()); + } catch (const bi::bad_alloc&) { + } + + reassign(); + } + } + + /** + * @brief add a new directory to the tree + * + * @param name name of the file, expected to be relative to this directory + * @param data the file data to attach + * @param flags flags for this files + * @param overwrite if true, the new leaf will overwrite an existing one that compares + *as "equal" + * @return pointer to the new node or a null ptr + **/ + template + typename TreeT::NodePtrT addDirectory(const fs::path& name, const T& data, + TreeFlags flags = 0, bool overwrite = true) + { + for (;;) { + DecomposablePath dp(name.string()); + + try { + return addNode(m_TreeMeta->tree.get(), dp, data, overwrite, + flags | FLAG_DIRECTORY, allocator()); + } catch (const bi::bad_alloc&) { + } + + reassign(); + } + } + + void getBuffer(void*& buffer, size_t& bufferSize) const + { + buffer = m_SHM->get_address(); + bufferSize = m_SHM->get_size(); + } + +private: + struct TreeMeta + { + TreeMeta(const typename TreeT::DataT& data, SegmentManagerT* segmentManager) + : tree(segmentManager->construct(bi::anonymous_instance)( + "", true, TreeT::NodePtrT(), data, VoidAllocatorT(segmentManager))), + referenceCount(0), // reference count only set on top level node + outdated(false) + {} + + OffsetPtrT tree; + long referenceCount; + bool outdated; + bi::interprocess_mutex mutex; + }; + + std::string m_SHMName; + std::shared_ptr m_SHM; + TreeMeta* m_TreeMeta; + + typename TreeT::DataT createEmpty() + { + return createDataEmpty(allocator()); + } + + template + TreeT* createSubNode(const VoidAllocatorT& allocator, std::string_view name, + unsigned long flags, const T& data) + { + auto* manager = allocator.get_segment_manager(); + + return manager->construct(bi::anonymous_instance)( + name, flags, TreeT::NodePtrT(), + createData(data, allocator), manager); + } + + typename TreeT::NodePtrT createSubPtr(TreeT* subNode) + { + SharedMemoryT::segment_manager* manager = m_SHM->get_segment_manager(); + return TreeT::NodePtrT(subNode, allocator(), TreeT::DeleterT(manager)); + } + + template + typename TreeT::NodePtrT addNode(TreeT* base, DecomposablePath& path, const T& data, + bool overwrite, unsigned int flags, + const VoidAllocatorT& allocator) + { + if (!path.peekNext()) { + typename TreeT::NodePtrT newNode = base->node(path.current()); + + if (!newNode) { + // last name component, should be the filename + TreeT* node = createSubNode(allocator, path.current(), flags, data); + newNode = createSubPtr(node); + newNode->m_Self = TreeT::WeakPtrT(newNode); + newNode->m_Parent = base->m_Self; + base->set(StringT(path.current(), allocator), newNode); + return newNode; + } else if (overwrite) { + newNode->m_Data = createData(data, allocator); + newNode->m_Flags = static_cast(flags); + return newNode; + } else { + // the node is already in the tree, overwrite is false, nothing to do + return {}; + } + } else { + // not last component, continue search in child node + auto subNode = base->m_Nodes.find(path.current()); + + if (subNode == base->m_Nodes.end()) { + typename TreeT::NodePtrT newNode = createSubPtr(createSubNode( + allocator, path.current(), FLAG_DIRECTORY | FLAG_DUMMY, createEmpty())); + + subNode = + base->m_Nodes.emplace(StringT(path.current(), allocator), newNode).first; + subNode->second->m_Self = TreeT::WeakPtrT(subNode->second); + subNode->second->m_Parent = base->m_Self; + } + + path.next(); + + return addNode(subNode->second.get().get(), path, data, overwrite, flags, + allocator); + } + } + + /** + * @brief copy content of one tree to a different tree (in a different shared memory + * segment + * @param destination + * @param reference + * @note at the time this is called, destination needs to refer to the shm of + * "destination" so that objects can be allocated in the new tree + */ + void copyTree(TreeT* destination, const TreeT* reference) + { + VoidAllocatorT allocator = VoidAllocatorT(m_SHM->get_segment_manager()); + destination->m_Flags = reference->m_Flags; + dataAssign(destination->m_Data, reference->m_Data); + destination->m_Name.assign(reference->m_Name.c_str()); + + for (const auto& kv : reference->m_Nodes) { + TreeT* newNode = createSubNode(allocator, "", true, createEmpty()); + typename TreeT::NodePtrT newNodePtr = createSubPtr(newNode); + + // need to set self BEFORE recursively copying the subtree, otherwise + // how would we assign parent pointers? + newNode->m_Self = newNodePtr; + + TreeT* source = reinterpret_cast(kv.second.get().get()); + copyTree(newNode, source); + destination->set(newNode->m_Name, newNodePtr); + newNode->m_Parent = destination->m_Self; + } + } + + int increaseRefCount(TreeMeta* treeMeta) + { + bi::scoped_lock lock(treeMeta->mutex); + return ++treeMeta->referenceCount; + } + + int decreaseRefCount(TreeMeta* treeMeta) + { + bi::scoped_lock lock(treeMeta->mutex); + return --treeMeta->referenceCount; + } + + // see activateSHM() for return value + // + std::optional createOrOpen(const std::string& SHMName, size_t size) + { + SharedMemoryT* newSHM = openSHM(SHMName); + + if (newSHM) { + spdlog::get("usvfs")->info("{} opened in process {}", SHMName, + ::GetCurrentProcessId()); + } else { + newSHM = createSHM(SHMName, size); + + if (newSHM) { + spdlog::get("usvfs")->info("{} created in process {}", SHMName, + ::GetCurrentProcessId()); + } + } + + if (!newSHM) { + spdlog::get("usvfs")->error("failed to create or open {} in process {}", SHMName, + ::GetCurrentProcessId()); + + throw std::exception("no shm instance"); + } + + return activateSHM(newSHM, SHMName); + } + + // see activateSHM() for return value + // + SharedMemoryT* createSHM(const std::string& SHMName, size_t size) + { + try { + return new SharedMemoryT(bi::create_only, SHMName.c_str(), + static_cast(size)); + } catch (const bi::interprocess_exception&) { + } + + return nullptr; + } + + // see activateSHM() for return value + // + SharedMemoryT* openSHM(const std::string& SHMName) + { + try { + return new SharedMemoryT(bi::open_only, SHMName.c_str()); + } catch (const bi::interprocess_exception&) { + } + + return nullptr; + } + + // makes the given shm current, returns the name of the previous shm block + // if it is now unused and must be destroyed; if the block is still used by + // another process, returns empty + // + // see reassign() + // + std::optional activateSHM(SharedMemoryT* shm, const std::string& SHMName) + { + std::shared_ptr oldSHM = m_SHM; + + m_SHM.reset(shm); + std::pair res = m_SHM->find("Meta"); + + if (res.first == nullptr) { + res.first = m_SHM->construct("Meta")(createEmpty(), + m_SHM->get_segment_manager()); + if (res.first == nullptr) { + USVFS_THROW_EXCEPTION(bi::bad_alloc()); + } + if (m_TreeMeta != nullptr) { + copyTree(res.first->tree.get(), m_TreeMeta->tree.get()); + } + } + + increaseRefCount(res.first); + + std::optional deadSHMName; + + if (oldSHM.get() != nullptr) { + const bool lastUser = unassign(oldSHM, m_TreeMeta); + if (lastUser) { + deadSHMName = m_SHMName; + } + } + + m_TreeMeta = res.first; + m_SHMName = SHMName; + + return deadSHMName; + } + + static std::string followupName(const std::string& currentName) + { + std::regex pattern(R"exp((.*_)(\d+))exp"); + std::smatch match; + regex_match(currentName, match, pattern); + + if (match.size() != 3) { + USVFS_THROW_EXCEPTION(usage_error() << ex_msg("shared memory name invalid")); + } + + const int count = boost::lexical_cast(match[2]); + return match[1].str() + std::to_string(count + 1); + } + + bool unassign(const std::shared_ptr& shm, TreeMeta* tree) + { + if (tree == nullptr) { + return true; + } + + if (decreaseRefCount(tree) == 0) { + shm->get_segment_manager()->destroy_ptr(tree); + return true; + } else { + return false; + } + } + + // re-entrancy: every time a process switches to a new shared memory block, it + // will know whether it was the last process to have a handle to it; when that + // happens, the block will be destroyed to avoid leaking it + // + // destroying these blocks is somewhat dangerous: it ends up in boost, which + // will try to access the filesystem to see if the name of the shared memory + // corresponds to a file on the drive, which can call hooked functions and end + // up right back here + // + // (note that in usvfs, only the shared memory for the log file uses a real + // file on the filesystem, see shmlogger.cpp; all the tree stuff uses + // anonymous, memory mapped files that live in the Windows pagefile) + // + // so the old blocks can be destroyed, but only after all the shenanigans with + // finding the correct shared memory block are over and `m_TreeMeta` points to + // a valid block, so all the names of the dead shared memory blocks are kept + // in a vector and deallocated at the very end + // + void reassign() + { + // list of all the shared memory blocks that are now unused and can be + // destroyed + std::vector deadSHMNames; + + if (m_TreeMeta->outdated) { + // this block was marked as outdated, which should only happen when + // another process has ran out of memory and started allocating blocks + // with higher numbers + + spdlog::get("usvfs")->info("tree {0} is outdated, looking for another one", + m_SHMName); + + if (findNewerBlock(deadSHMNames)) { + // the new block was found and activated + return; + } + } else { + // this block isn't outdated, so reassign() was called because a bad_alloc + // exception was thrown + spdlog::get("usvfs")->info( + "ran out of memory in tree {0}, will create another one", m_SHMName); + } + + // either the block is full or it's outdated, but no higher block was found; + // just create a new one + + createNewBlock(deadSHMNames); + + // remove the old shared memory blocks; this can be recursive and call + // reassign() again, but it's safe at this point + for (const std::string& name : deadSHMNames) { + spdlog::get("usvfs")->info("destroying {0}", name); + bi::shared_memory_object::remove(name.c_str()); + } + } + + // tries a series of shm names above the current one in the hope of finding + // the most recent one + // + // if the new block was found and activated correctly, returns true + // + // when findNewerBlock() returns false, this container will either still be + // pointing to the same, outdated block as it started with, or it might have + // moved up to another outdated block, but it will always be pointing to + // a valid block in memory + // + bool findNewerBlock(std::vector& deadSHMNames) + { + // how many blocks are checked above the current one; it's unlikely there + // will ever be more than 15 to 20 blocks allocated, but this is high + // because it's really the only way to keep processes connected to the same + // shm block, so processes must not fail to find the next one + constexpr int Tries = 100; + + std::string nextName = m_SHMName; + + for (int i = 0; i < Tries; ++i) { + // the shm name is something like "mod_organizer_3", which becomes + // "mod_organizer_4" + nextName = followupName(nextName); + spdlog::get("usvfs")->info("opening {0}", nextName); + + // open the shm, see if it exists + SharedMemoryT* shm = openSHM(nextName); + + if (!shm) { + // this is not necessarily an error, another process might have created + // a bunch of blocks and destroyed them before this process had a chance + // to see them, so just keep going + spdlog::get("usvfs")->info("{0} doesn't exist", nextName); + continue; + } + + spdlog::get("usvfs")->info("{0} exists, activating", nextName); + const auto deadSHMName = activateSHM(shm, nextName); + + // if this process was the last user of the previous block, it must be + // deallocated, but only after this whole thing is finished, because it + // can end up calling reassign() again + if (deadSHMName) { + spdlog::get("usvfs")->info("will destroy {0}", *deadSHMName); + deadSHMNames.push_back(*deadSHMName); + } + + // another process might have already created this block, run out of + // memory and created more, so make sure to only stop when finding a block + // that's not outdated + if (m_TreeMeta->outdated) { + spdlog::get("usvfs")->info("{0} is also outdated", nextName); + } else { + // shm opened correctly, activated and not outdated, done + spdlog::get("usvfs")->info("{0} not outdated, taking it, size now {1}", + nextName, byte_string(m_SHM->get_size())); + + return true; + } + } + + // this block is outdated, but no other valid block was found above; this + // shouldn't happen, but just create a new block to make sure programs can + // still run + + spdlog::get("usvfs")->error( + "found no existing tree above {0}, will create a new one", m_SHMName); + + return false; + } + + // creates a new block and activates it, throws on failure + // + void createNewBlock(std::vector& deadSHMNames) + { + // the current block is now considered stale, so make sure other processes + // are aware of it and try to find the new block + // + // todo: there really should be some synchronization here, another process + // can pick up this flag before the next block has been created + m_TreeMeta->outdated = true; + + // the shm name is something like "mod_organizer_3", which becomes + // "mod_organizer_4" + const std::string nextName = followupName(m_SHMName); + spdlog::get("usvfs")->info("creating {0}", nextName); + + SharedMemoryT* shm = createSHM(nextName, m_SHM->get_size() * 2); + + if (!shm) { + // this shouldn't happen + spdlog::get("usvfs")->error("failed to create {0}", nextName); + throw std::exception("cannot create block"); + } + + spdlog::get("usvfs")->info("{0} created, activating", nextName); + const auto deadSHMName = activateSHM(shm, nextName); + + // if this process was the last user of the previous block, it must be + // deallocated, but only after this whole thing is finished, because it + // can end up calling reassign() again + if (deadSHMName) { + spdlog::get("usvfs")->info("will destroy {0}", *deadSHMName); + deadSHMNames.push_back(*deadSHMName); + } + + spdlog::get("usvfs")->info("tree {0} size now {1}", m_SHMName, + byte_string(m_SHM->get_size())); + } +}; + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/unicodestring.cpp b/libs/usvfs/src/shared/unicodestring.cpp new file mode 100644 index 0000000..f997e7f --- /dev/null +++ b/libs/usvfs/src/shared/unicodestring.cpp @@ -0,0 +1,77 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "unicodestring.h" +#include "logging.h" +#include "stringcast.h" +#include "stringutils.h" + +namespace usvfs +{ + +UnicodeString::UnicodeString(LPCWSTR string, size_t length) +{ + if (length == std::string::npos) { + length = wcslen(string); + } + m_Buffer.resize(length + 1); + memcpy(m_Buffer.data(), string, length * sizeof(wchar_t)); + update(); +} + +UnicodeString::UnicodeString(const std::wstring& string) +{ + m_Buffer.resize(string.length() + 1); + memcpy(m_Buffer.data(), string.data(), string.length() * sizeof(wchar_t)); + update(); +} + +UnicodeString& UnicodeString::operator=(const std::wstring& string) +{ + m_Buffer.resize(string.length() + 1); + memcpy(m_Buffer.data(), string.data(), string.length() * sizeof(wchar_t)); + update(); + return *this; +} + +UnicodeString& UnicodeString::appendPath(PUNICODE_STRING path) +{ + if (path != nullptr && path->Buffer && path->Length) { + auto appendAt = size(); + if (appendAt) { + m_Buffer.resize(m_Buffer.size() + path->Length / sizeof(WCHAR) + 1); + m_Buffer[appendAt++] = L'\\'; + } else + m_Buffer.resize(path->Length / sizeof(WCHAR) + 1); + memcpy(&m_Buffer[appendAt], path->Buffer, path->Length); + update(); + } + + return *this; +} + +void UnicodeString::update() +{ + m_Data.Length = static_cast(size() * sizeof(WCHAR)); + m_Data.MaximumLength = static_cast((m_Buffer.capacity() - 1) * sizeof(WCHAR)); + m_Data.Buffer = m_Buffer.data(); +} + +} // namespace usvfs diff --git a/libs/usvfs/src/shared/unicodestring.h b/libs/usvfs/src/shared/unicodestring.h new file mode 100644 index 0000000..77071ad --- /dev/null +++ b/libs/usvfs/src/shared/unicodestring.h @@ -0,0 +1,110 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "ntdll_declarations.h" +#include "windows_sane.h" + +#include "formatters.h" + +namespace usvfs +{ + +/** + * @brief C++ wrapper for the windows UNICODE_STRING structure + */ +class UnicodeString +{ +public: + UnicodeString() : m_Buffer(1) { update(); } + + UnicodeString(const UnicodeString& other) : m_Buffer(other.m_Buffer) { update(); } + + UnicodeString(UnicodeString&& other) : m_Buffer(std::move(other.m_Buffer)) + { + update(); + } + + UnicodeString(const std::wstring& string); + UnicodeString(LPCWSTR string, size_t length = std::string::npos); + + UnicodeString& operator=(const std::wstring& string); + + UnicodeString& operator=(const UnicodeString& other) + { + m_Buffer = other.m_Buffer; + update(); + return *this; + } + + UnicodeString& operator=(UnicodeString&& other) + { + m_Buffer = std::move(other.m_Buffer); + update(); + return *this; + } + + /** + * @brief convert to a WinNt Api-style unicode string. This is only valid as long + * as the string isn't modified + */ + explicit operator PUNICODE_STRING() { return &m_Data; } + + /** + * @brief convert to a Win32 Api-style unicode string. This is only valid as long + * as the string isn't modified + */ + explicit operator LPCWSTR() const { return m_Data.Buffer; } + + /** + * @return length of the string in 16-bit words (not including zero termination) + */ + size_t size() const { return m_Buffer.size() - 1; } + + wchar_t operator[](size_t pos) const { return m_Buffer[pos]; } + + UnicodeString& appendPath(PUNICODE_STRING path); + +private: + friend struct ::std::formatter; + + void update(); + + UNICODE_STRING m_Data; + std::vector m_Buffer; +}; + +} // namespace usvfs + +template <> +struct std::formatter + : std::formatter +{ + template + FmtContext::iterator format(const usvfs::UnicodeString& v, FmtContext& ctx) const + { + if (v.size() == 0) { + return std::format_to(ctx.out(), ""); + } else { + return std::formatter::format(&v.m_Data, ctx); + } + } +}; diff --git a/libs/usvfs/src/shared/wildcard.cpp b/libs/usvfs/src/shared/wildcard.cpp new file mode 100644 index 0000000..a2efe6d --- /dev/null +++ b/libs/usvfs/src/shared/wildcard.cpp @@ -0,0 +1,194 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "wildcard.h" +#include "logging.h" +#include "windows_sane.h" + +static bool IsInnerMatch(LPCWSTR pszString, LPCWSTR pszMatch) +{ + while (*pszMatch != L'\0') { + if ((*pszMatch == L'?') || (*pszMatch == L'>')) { + if (!*pszString) { + // ? must match exactly one character + return false; + } + + ++pszString; + ++pszMatch; + } else if ((*pszMatch == L'*') || (*pszMatch == L'<')) { + if (IsInnerMatch(pszString, pszMatch + 1)) { + // * may match empty string or we may have matched something already + return true; + } + + return *pszString && IsInnerMatch(pszString + 1, pszMatch); + + // the rest of the string can't be matched + } else { + if (CharUpperW(MAKEINTRESOURCEW(MAKELONG(*pszString++, 0))) != + CharUpperW(MAKEINTRESOURCEW(MAKELONG(*pszMatch++, 0)))) { + // regular chars compare + return false; + } + } + } + + return !*pszString && !*pszMatch; +} + +static LPCSTR InnerMatch(LPCSTR pszString, LPCSTR pszMatch) +{ + // We have a special case where string is empty ("") and the mask is "*". + // We need to handle this too. So we can't test on !*pszString here. + // The loop breaks when the match string is exhausted. + while (*pszString != '\0') { + // Single wildcard character + if ((*pszMatch == '?') || (*pszMatch == '>')) { + // can't match directory separator + if ((*pszString == '\\') || (*pszString == '/')) + return nullptr; + + // Matches any character except empty string + if (*pszString == '\0') { + // string consumed, part of the pattern is left + return pszMatch; + } + + // OK next + ++pszString; + ++pszMatch; + } else if ((*pszMatch == '*') || (*pszMatch == '<')) { + // * doesn't match directory separators + if ((*pszString == '\\') || (*pszString == '/')) { + ++pszMatch; + continue; + } + + // Need to do some tricks. + + // 1. The wildcard * is ignored. + // So just an empty string matches. This is done by recursion. + // Because we eat one character from the match string, the + // recursion will stop. + { + LPCSTR remainder = InnerMatch(pszString, pszMatch + 1); + if (remainder != nullptr) { + // we have a match and the * replaces no other character + return remainder; + } + } + + // 2. Chance we eat the next character and try it again, with a + // wildcard * match. This is done by recursion. Because we eat + // one character from the string, the recursion will stop. + if (*pszString != '\0') { + LPCSTR remainder = InnerMatch(pszString + 1, pszMatch); + if (remainder != nullptr) { + return remainder; + } + } + + // Nothing worked with this wildcard. + return nullptr; + } else { + // Standard compare of 2 chars. Note that *pszSring might be 0 + // here, but then we never get a match on *pszMask that has always + // a value while inside this loop. + if (CharUpperA(MAKEINTRESOURCEA(MAKELONG(*pszString++, 0))) != + CharUpperA(MAKEINTRESOURCEA(MAKELONG(*pszMatch++, 0)))) + return nullptr; + } + } + + // successful match if the input string was completely consumed + if (*pszString == '\0') { + while ((*pszMatch == '*') || (*pszMatch == '<')) { + ++pszMatch; + } + return pszMatch; + } else { + return nullptr; + } +} + +namespace usvfs::shared::wildcard +{ + +bool Match(LPCWSTR pszString, LPCWSTR pszMatch) +{ + if (*pszString == L'.') { + // cmd.exe seems to ignore + return Match(pszString + 1, pszMatch); + } else { + size_t len = wcslen(pszMatch); + if ((len > 2) && (wcscmp(pszMatch + len - 2, L".*") == 0)) { + // cmd.exe seems to completely ignore .* at the end. + std::wstring temp(pszMatch, pszMatch + len - 2); + return IsInnerMatch(pszString, temp.c_str()); + } + return IsInnerMatch(pszString, pszMatch); + } +} + +bool Match(LPCSTR pszString, LPCSTR pszMatch) +{ + if (*pszString == '.') { + // cmd.exe seems to ignore + return Match(pszString + 1, pszMatch); + } else { + size_t len = strlen(pszMatch); + LPCSTR res = nullptr; + if ((len > 2) && (strcmp(pszMatch + len - 2, ".*") == 0)) { + // cmd.exe seems to completely ignore .* at the end. + std::string temp(pszMatch, pszMatch + len - 2); + res = InnerMatch(pszString, temp.c_str()); + } else { + res = InnerMatch(pszString, pszMatch); + } + return ((res != nullptr) && (*res == '\0')); + } +} + +LPCSTR PartialMatch(LPCSTR pszString, LPCSTR pszMatch) +{ + if (*pszString == '.') { + // cmd.exe seems to ignore dots at the start + return PartialMatch(pszString + 1, pszMatch); + } else { + size_t len = strlen(pszMatch); + if ((len > 2) && (strcmp(pszMatch + len - 2, ".*") == 0)) { + // in cmd.exe there seems to be no difference between * and + // *.* + std::string temp(pszMatch, pszMatch + len - 2); + LPCSTR pos = InnerMatch(pszString, temp.c_str()); + if (pos != nullptr) { + if (*pos == '\0') { + return pszMatch + strlen(pszMatch); + } else { + return pszMatch + (pos - temp.c_str()); + } + } + } + return InnerMatch(pszString, pszMatch); + } +} + +} // namespace usvfs::shared::wildcard diff --git a/libs/usvfs/src/shared/wildcard.h b/libs/usvfs/src/shared/wildcard.h new file mode 100644 index 0000000..bf5b4a3 --- /dev/null +++ b/libs/usvfs/src/shared/wildcard.h @@ -0,0 +1,67 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +/// Wildcard matching code +/// by Martin Richter +/// licensed under The Code Project Open License (CPOL) + +#pragma once + +#include "windows_sane.h" + +namespace usvfs::shared::wildcard +{ + +/** + * @brief match string to wildcard windows-style + * @param pszString Input string to match + * @param pszMatch Match mask that may contain wildcards like ? and * + * @note A ? sign matches any character, except an empty string. + * @note A * sign matches any string inclusive an empty string. + * @note Characters are compared caseless. + * @return true if the string matches the pattern + */ +bool Match(LPCWSTR pszString, LPCWSTR pszMatch); + +/** + * @brief match string to wildcard windows-style + * @param pszString Input string to match + * @param pszMatch Match mask that may contain wildcards like ? and * + * @note A ? sign matches any character, except an empty string. + * @note A * sign matches any string inclusive an empty string. + * @note Characters are compared caseless. + * @return true if the string matches the pattern + */ +bool Match(LPCSTR pszString, LPCSTR pszMatch); + +/** + * @brief match string to wildcard windows-style + * @param pszString Input string to match + * @param pszMatch Match mask that may contain wildcards like ? and * + * @note A ? sign matches any character, except an empty string. + * @note A * sign matches any string inclusive an empty string. + * @note Characters are compared caseless. + * @return the "not-consumed" remainder of the pattern. If this points to a + * zero terminator, this was a full match. + * Returns nullptr if no match is possible + */ +LPCSTR PartialMatch(LPCSTR pszString, LPCSTR pszMatch); + +} // namespace usvfs::shared::wildcard diff --git a/libs/usvfs/src/shared/winapi.cpp b/libs/usvfs/src/shared/winapi.cpp new file mode 100644 index 0000000..e28a13c --- /dev/null +++ b/libs/usvfs/src/shared/winapi.cpp @@ -0,0 +1,490 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "winapi.h" +#include "exceptionex.h" +#include "logging.h" +#include "ntdll_declarations.h" +#include "stringcast.h" +#include "stringutils.h" +#include "unicodestring.h" + +namespace winapi::ansi +{ + +std::string getModuleFileName(HMODULE module, HANDLE process) +{ + std::wstring result = wide::getModuleFileName(module, process); + return usvfs::shared::string_cast(result); +} + +std::string getCurrentDirectory() +{ + std::string result; + DWORD required = GetCurrentDirectoryA(0, nullptr); + if (required == 0UL) { + throw usvfs::shared::windows_error("failed to determine current working directory"); + } + result.resize(required); + GetCurrentDirectoryA(required, &result[0]); + result.resize(required - 1); + return result; +} + +std::pair getFullPathName(LPCSTR fileName) +{ + static const int INIT_SIZE = 128; + std::string result; + result.resize(INIT_SIZE); + LPSTR filePart = nullptr; + DWORD requiredSize = GetFullPathNameA(fileName, INIT_SIZE, &result[0], &filePart); + if (requiredSize >= INIT_SIZE) { + result.resize(requiredSize); + GetFullPathNameA(fileName, requiredSize, &result[0], &filePart); + } + if (requiredSize != 0UL) { + return std::make_pair(result, std::string(filePart != nullptr ? filePart : "")); + } else { + return make_pair(result, std::string()); + } +} + +} // namespace winapi::ansi + +namespace winapi::wide +{ + +std::wstring getModuleFileName(HMODULE module, HANDLE process) +{ + std::wstring result; + result.resize(64); + DWORD rc = 0UL; + + while ((rc = (process == INVALID_HANDLE_VALUE) + ? ::GetModuleFileNameW(module, &result[0], + static_cast(result.size())) + : ::GetModuleFileNameExW(process, module, &result[0], + static_cast(result.size()))) == + result.size()) { + result.resize(result.size() * 2); + } + + if (rc == 0UL) { + if (::GetLastError() == ERROR_PARTIAL_COPY) { +#if BOOST_ARCH_X86_64 + return L"unknown (32-bit process)"; +#else + return L"unknown (64-bit process)"; +#endif + } else { + throw usvfs::shared::windows_error("failed to retrieve module file name"); + } + } + + result.resize(rc); + + return result; +} + +std::pair getFullPathName(LPCWSTR fileName) +{ + wchar_t buf1[MAX_PATH]; + std::vector buf2; + wchar_t* result = buf1; + LPWSTR filePart = nullptr; + DWORD requiredSize = GetFullPathNameW(fileName, MAX_PATH, result, &filePart); + if (requiredSize >= MAX_PATH) { + buf2.resize(requiredSize); + result = &buf2[0]; + requiredSize = GetFullPathNameW(fileName, requiredSize, result, &filePart); + } + return make_pair(std::wstring(result, requiredSize), + std::wstring((requiredSize && filePart) ? filePart : L"")); +} + +std::wstring getCurrentDirectory() +{ + // really great api this (::GetCurrentDirectoryW) + // - if it succeeds, returns size in characters WITHOUT zero termination + // - if it fails due to buffer too small, returns size in characters WITH zero + // termination + // - if it fails for other reasons, returns 0 + std::wstring result; + DWORD required = GetCurrentDirectoryW(0, nullptr); + if (required == 0UL) { + throw usvfs::shared::windows_error("failed to determine current working directory"); + } + result.resize(required); + GetCurrentDirectoryW(required, &result[0]); + result.resize(required - 1); + return result; +} + +std::wstring getKnownFolderPath(REFKNOWNFOLDERID folderID) +{ + PWSTR writablePath; + + ::SHGetKnownFolderPath(folderID, 0, nullptr, &writablePath); + + ON_BLOCK_EXIT([writablePath]() { + ::CoTaskMemFree(writablePath); + }); + + return std::wstring(writablePath); +} + +} // namespace winapi::wide + +namespace winapi::ex +{ + +std::pair getSectionRange(HANDLE moduleHandle) +{ + std::pair result; + bool found = false; + uintptr_t exeModule = reinterpret_cast(moduleHandle); + if (exeModule == 0) { + throw std::runtime_error("failed to determine address range of executable"); + } + + std::pair totalRange{UINT_MAX, 0}; + + PIMAGE_DOS_HEADER dosHeader = reinterpret_cast(exeModule); + PIMAGE_NT_HEADERS ntHeader = + reinterpret_cast(exeModule + dosHeader->e_lfanew); + PIMAGE_SECTION_HEADER sectionHeader = + reinterpret_cast(ntHeader + 1); + for (int i = 0; i < ntHeader->FileHeader.NumberOfSections && !found; ++i) { + if (memcmp(sectionHeader->Name, ".text", 5) == 0) { + result.first = exeModule + sectionHeader->VirtualAddress; + result.second = result.first + sectionHeader->Misc.VirtualSize; + found = true; + } else { + uintptr_t start = exeModule + sectionHeader->VirtualAddress; + totalRange.first = std::min(totalRange.first, start); + totalRange.second = std::max(totalRange.second, + start + sectionHeader->Misc.VirtualSize); + } + ++sectionHeader; + } + + if (!found) { + return totalRange; + } + + return result; +} + +OSVersion getOSVersion() +{ + RTL_OSVERSIONINFOEXW versionInfo; + ZeroMemory(&versionInfo, sizeof(RTL_OSVERSIONINFOEXW)); + versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW); + RtlGetVersion((PRTL_OSVERSIONINFOW)&versionInfo); + + OSVersion result; + result.major = versionInfo.dwMajorVersion; + result.minor = versionInfo.dwMinorVersion; + result.build = versionInfo.dwBuildNumber; + result.platformid = versionInfo.dwPlatformId; + result.servicpack = + versionInfo.wServicePackMajor << 16 | versionInfo.wServicePackMinor; + return result; +} + +} // namespace winapi::ex + +namespace winapi::ex::ansi +{ + +std::string errorString(DWORD errorCode) +{ + std::ostringstream finalMessage; + + LPSTR buffer = nullptr; + + DWORD currentErrorCode = GetLastError(); + + errorCode = + errorCode != std::numeric_limits::max() ? errorCode : currentErrorCode; + + // TODO: the message is not english? + if (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + nullptr, errorCode, + 0 //, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT) + , + (LPSTR)&buffer, 0, nullptr) == 0) { + finalMessage << "(unknown error [" << errorCode << "])"; + } else { + if (buffer != nullptr) { + size_t end = strlen(buffer) - 1; + while ((buffer[end] == '\n') || (buffer[end] == '\r')) { + buffer[end--] = '\0'; + } + finalMessage << "(" << buffer << " [" << errorCode << "])"; + LocalFree(buffer); // allocated by FormatMessage + } + } + + SetLastError(currentErrorCode); // restore error code because FormatMessage might + // have modified it + return finalMessage.str(); +} + +std::string toString(const FILETIME& time) +{ + SYSTEMTIME temp; + FileTimeToSystemTime(&time, &temp); + std::ostringstream stream; + stream << temp.wYear << "-" << temp.wMonth << "-" << temp.wDay << temp.wHour << ":" + << temp.wMinute << ":" << temp.wSecond; + return stream.str(); +} + +LPCSTR GetBaseName(LPCSTR string) +{ + LPCSTR result = string + strlen(string) - 1; + while (result > string) { + if ((*result == '\\') || (*result == '/')) { + ++result; + break; + } else { + --result; + } + } + return result; +} + +} // namespace winapi::ex::ansi + +namespace winapi::ex::wide +{ + +bool fileExists(LPCWSTR fileName, bool* isDirectory) +{ + DWORD attrib = GetFileAttributesW(fileName); + + if (attrib == INVALID_FILE_ATTRIBUTES) { + return false; + } else { + if (isDirectory != nullptr) { + *isDirectory = (attrib & FILE_ATTRIBUTE_DIRECTORY) != 0; + } + return true; + } +} + +std::wstring errorString(DWORD errorCode) +{ + std::wostringstream finalMessage; + + LPWSTR buffer = nullptr; + + DWORD currentErrorCode = GetLastError(); + + errorCode = + errorCode != std::numeric_limits::max() ? errorCode : currentErrorCode; + + // TODO: the message is not english? + if (FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + nullptr, errorCode, + 0 //, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT) + , + (LPWSTR)&buffer, 0, nullptr) == 0) { + finalMessage << L"(unknown error [" << errorCode << "])"; + } else { + if (buffer != nullptr) { + size_t end = wcslen(buffer) - 1; + while ((buffer[end] == L'\n') || (buffer[end] == L'\r')) { + buffer[end--] = L'\0'; + } + finalMessage << L"(" << buffer << L" [" << errorCode << L"])"; + LocalFree(buffer); // allocated by FormatMessage + } + } + + SetLastError(currentErrorCode); // restore error code because FormatMessage might + // have modified it + return finalMessage.str(); +} + +std::wstring toString(const FILETIME& time) +{ + SYSTEMTIME temp; + FileTimeToSystemTime(&time, &temp); + std::wostringstream stream; + stream << temp.wYear << "-" << temp.wMonth << "-" << temp.wDay << temp.wHour << ":" + << temp.wMinute << ":" << temp.wSecond; + return stream.str(); +} + +LPCWSTR GetBaseName(LPCWSTR string) +{ + LPCWSTR result; + if ((string == nullptr) || (string[0] == L'\0')) { + result = string; + } else { + result = string + wcslen(string) - 1; + } + + while (result > string) { + if ((*result == L'\\') || (*result == L'/')) { + ++result; + break; + } else { + --result; + } + } + return result; +} + +LPWSTR GetBaseName(LPWSTR path) +{ + LPCWSTR result = GetBaseName(static_cast(path)); + return const_cast(result); +} + +std::wstring getSectionName(PVOID addressIn, HANDLE process) +{ + if (process == nullptr) { + process = GetCurrentProcess(); + } + HMODULE modules[1024]; + intptr_t address = reinterpret_cast(addressIn); + DWORD required; + if (::EnumProcessModules(process, modules, sizeof(modules), &required)) { + for (DWORD i = 0; i < (std::min(1024UL, required) / sizeof(HMODULE)); ++i) { + std::pair range = getSectionRange(modules[i]); + if ((address > range.first) && (address < range.second)) { + try { + return winapi::wide::getModuleFileName(modules[i], process); + } catch (const std::exception&) { + return std::wstring(L"unknown"); + } + } + } + } + return std::wstring(L"unknown"); +} + +std::vector quickFindFiles(LPCWSTR directoryName, LPCWSTR pattern) +{ + std::vector result; + + static const unsigned int BUFFER_SIZE = 1024; + + HANDLE hdl = CreateFileW(directoryName, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + + ON_BLOCK_EXIT([hdl]() { + CloseHandle(hdl); + }); + + uint8_t buffer[BUFFER_SIZE]; + + NTSTATUS res = STATUS_SUCCESS; // status success + while (res == STATUS_SUCCESS) { + IO_STATUS_BLOCK status; + + res = NtQueryDirectoryFile( + hdl, nullptr, nullptr, nullptr, &status, buffer, BUFFER_SIZE, + FileFullDirectoryInformation, FALSE, + static_cast(usvfs::UnicodeString(pattern)), FALSE); + if (res == STATUS_SUCCESS) { + FILE_FULL_DIR_INFORMATION* info = + reinterpret_cast(buffer); + void* endPos = buffer + status.Information; + while (info < endPos) { + FileResult file; + file.fileName = + std::wstring(info->FileName, info->FileNameLength / sizeof(wchar_t)); + file.attributes = info->FileAttributes; + + result.push_back(file); + if (info->NextEntryOffset == 0) { + break; + } else { + info = reinterpret_cast( + reinterpret_cast(info) + info->NextEntryOffset); + } + } + } + } + + return result; +} + +bool createPath(boost::filesystem::path path, LPSECURITY_ATTRIBUTES securityAttributes) +{ + // sanity and guaranteed recursion end: + if (!path.has_relative_path()) + throw usvfs::shared::windows_error( + "createPath() refusing to create non-existing top level path: " + + path.string()); + + DWORD attr = GetFileAttributesW(path.c_str()); + DWORD err = GetLastError(); + if (attr != INVALID_FILE_ATTRIBUTES) { + if (attr & FILE_ATTRIBUTE_DIRECTORY) + return false; // if directory already exists all is good + else + throw usvfs::shared::windows_error("createPath() called on a file: " + + path.string()); + } + if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND) + throw usvfs::shared::windows_error( + "createPath() GetFileAttributesW failed on: " + path.string(), err); + + if (err != ERROR_FILE_NOT_FOUND) // ERROR_FILE_NOT_FOUND means parent directory + // already exists + createPath(path.parent_path(), + securityAttributes); // otherwise create parent directory (recursively) + + BOOL res = CreateDirectoryW(path.c_str(), securityAttributes); + if (!res) { + err = GetLastError(); + throw usvfs::shared::windows_error( + "createPath() CreateDirectoryW failed on: " + path.string(), err); + } + return true; +} + +std::wstring getWindowsBuildLab(bool ex) +{ + HKEY hKey = nullptr; + auto res = RegOpenKeyExW(HKEY_LOCAL_MACHINE, + LR"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)", 0, + KEY_READ, &hKey); + if (res != ERROR_SUCCESS || !hKey) + return L"Opening HKLM Windows NT\\CurrentVersion failed?!"; + WCHAR buf[200]; + DWORD size = static_cast(sizeof(buf)); + res = RegQueryValueExW(hKey, ex ? L"BuildLabEx" : L"BuildLab", NULL, NULL, + reinterpret_cast(buf), &size); + if (res != ERROR_SUCCESS || size > sizeof(buf)) + return ex ? L"BuildLabEx reg value not found?!" : L"BuildLab reg value not found?!"; + size /= sizeof(buf[0]); + if (size && !buf[size - 1]) + --size; + return std::wstring(buf, size); +} + +} // namespace winapi::ex::wide diff --git a/libs/usvfs/src/shared/winapi.h b/libs/usvfs/src/shared/winapi.h new file mode 100644 index 0000000..f94eff6 --- /dev/null +++ b/libs/usvfs/src/shared/winapi.h @@ -0,0 +1,610 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "logging.h" +#include "stringcast.h" +#include "windows_sane.h" +#include + +#define ALIAS(alias, original) \ + template \ + auto alias(Args&&... args) -> decltype(original(std::forward(args)...)) \ + { \ + return original(std::forward(args)...); \ + } + +#define ALIAST(alias, original) \ + template \ + auto alias(Args && ... args) \ + -> decltype(original(std::forward(args)...)) \ + { \ + return original(std::forward(args)...); \ + } + +namespace winapi +{ + +struct parameter_error : public std::runtime_error +{ + parameter_error(const std::string& msg) : runtime_error(msg) {} +}; + +} // namespace winapi + +namespace winapi::process +{ + +/** + * @brief result of process creation + */ +struct Result +{ + Result() + { + ::ZeroMemory(&processInfo, sizeof(PROCESS_INFORMATION)); + ::ZeroMemory(&startupInfo, sizeof(STARTUPINFO)); + startupInfo.cb = sizeof(STARTUPINFO); + } + + Result(Result&& reference) + : valid(reference.valid), startupInfo(reference.startupInfo), + processInfo(reference.processInfo), errorCode(reference.errorCode) + { + reference.valid = false; + } + + ~Result() + { + if (valid) { + CloseHandle(processInfo.hProcess); + CloseHandle(processInfo.hThread); + } + + if (stdoutPipe != INVALID_HANDLE_VALUE) { + CloseHandle(stdoutPipe); + } + } + + Result(const Result&) = delete; + + size_t readStdout(std::vector& buffer, bool& eof) + { + if (stdoutPipe != INVALID_HANDLE_VALUE) { + DWORD read; + BOOL res = ReadFile(stdoutPipe, &buffer[0], static_cast(buffer.size()), + &read, nullptr); + eof = (res == TRUE) && (read == 0); + return static_cast(read); + } else { + eof = true; + return 0; + } + } + + bool valid{false}; + STARTUPINFO startupInfo; + PROCESS_INFORMATION processInfo; + DWORD errorCode{0UL}; + + HANDLE stdoutPipe{INVALID_HANDLE_VALUE}; +}; + +/** + * @brief internal class to handle process creation with named parameters. + */ +template +class _Create +{ +public: + _Create(const std::basic_string& binaryName); + _Create(const _Create& reference) = delete; + _Create& operator=(const _Create& reference) = delete; + + _Create(_Create&& reference) + : m_CurrentDirectory(std::move(reference.m_CurrentDirectory)), + m_ProcessAttributes(reference.m_ProcessAttributes), + m_ThreadAttributes(reference.m_ThreadAttributes), + m_InheritHandles(reference.m_InheritHandles), + m_CreationFlags(std::move(reference.m_CreationFlags)), + m_Executed(reference.m_Executed) + { + // stringstream should be moveable but it seems it isn't on mingw + m_CommandLine << reference.m_CommandLine.rdbuf(); + } + + /// named parameter "argument". May be called repeatedly. This is + /// directly appended to the command line with a separating space. No + /// quoting happens + template + _Create& argument(const ArgT& argin) + { + m_CommandLine << " " << argin; + return *this; + } + + template + _Create& arg(const ArgT& argin) + { + return this->argument(argin); + } + + template + _Create& arguments(IterT begin, IterT end) + { + for (; begin != end; ++begin) { + m_CommandLine << " " << *begin; + } + + return *this; + } + + /// @brief set the working directory for the process + _Create& workingDirectory(const std::basic_string& path); + + /// @brief set process attributes + _Create& processAttributes(SECURITY_ATTRIBUTES* attributes); + + /// @brief set thread attributes + _Create& threadAttributes(SECURITY_ATTRIBUTES* attributes); + + /// @brief activate inheriting handles + _Create& inheritHandles(); + + /// @brief have the process start suspended + _Create& suspended(); + + /// @brief set the process up to output stout to a pipe which can be + /// retrieved through the result object + _Create& stdoutPipe(); + + /// @brief end the named parameter cascade and create the process + Result operator()() + { + m_CommandLine.seekp(0, std::ios::end); + unsigned int length = static_cast(m_CommandLine.tellp()); + std::unique_ptr clBuffer(new CharT[length + 1]); + memset(clBuffer.get(), 0, (length + 1) * sizeof(CharT)); + memcpy(clBuffer.get(), m_CommandLine.str().c_str(), length * sizeof(CharT)); + Result result; + + if (m_StdoutPipe) { + result.stdoutPipe = setupPipe(result.startupInfo.hStdOutput); + result.startupInfo.dwFlags |= STARTF_USESTDHANDLES; + } + + result.valid = + createProcessInt(nullptr, clBuffer.get(), m_ProcessAttributes, + m_ThreadAttributes, m_InheritHandles, m_CreationFlags, nullptr, + m_CurrentDirectory.length() > 0 ? m_CurrentDirectory.c_str() + : nullptr, + &result.startupInfo, &result.processInfo) == TRUE; + + if (m_Stdout != INVALID_HANDLE_VALUE) { + // got to close the write end of pipes + CloseHandle(result.startupInfo.hStdOutput); + } + + if (result.valid) { + result.errorCode = NOERROR; + } else { + result.errorCode = GetLastError(); + } + return result; + } + +private: + static BOOL createProcessInt(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + SECURITY_ATTRIBUTES* lpProcessAttributes, + SECURITY_ATTRIBUTES* lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, + LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation) + { + return ::CreateProcessW(lpApplicationName, lpCommandLine, lpProcessAttributes, + lpThreadAttributes, bInheritHandles, dwCreationFlags, + lpEnvironment, lpCurrentDirectory, lpStartupInfo, + lpProcessInformation); + } + + static BOOL createProcessInt(LPCSTR lpApplicationName, LPSTR lpCommandLine, + SECURITY_ATTRIBUTES* lpProcessAttributes, + SECURITY_ATTRIBUTES* lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, + LPVOID lpEnvironment, LPCSTR lpCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation) + { + std::wstring executable; + if (lpApplicationName != nullptr) { + executable = usvfs::shared::string_cast(lpApplicationName); + } + + std::wstring cmdline; + if (lpCommandLine != nullptr) { + cmdline = usvfs::shared::string_cast(lpCommandLine); + } + + std::wstring cwd; + if (lpCurrentDirectory != nullptr) { + cwd = usvfs::shared::string_cast(lpCurrentDirectory); + } + + return ::CreateProcessW(lpApplicationName != nullptr ? executable.c_str() : nullptr, + lpCommandLine != nullptr ? &cmdline[0] : nullptr, + lpProcessAttributes, lpThreadAttributes, bInheritHandles, + dwCreationFlags, lpEnvironment, + lpCurrentDirectory != nullptr ? cwd.c_str() : nullptr, + lpStartupInfo, lpProcessInformation); + } + + HANDLE setupPipe(HANDLE& childHandle) + { + SECURITY_ATTRIBUTES attr; + attr.nLength = sizeof(SECURITY_ATTRIBUTES); + attr.bInheritHandle = TRUE; + attr.lpSecurityDescriptor = nullptr; + + HANDLE pipe[2]; + + CreatePipe(&pipe[0], &pipe[1], &attr, 0); + SetHandleInformation(pipe[0], HANDLE_FLAG_INHERIT, 0); + + childHandle = pipe[1]; + + return pipe[0]; + } + +private: + std::basic_stringstream m_CommandLine; + std::basic_string m_CurrentDirectory{}; + SECURITY_ATTRIBUTES* m_ProcessAttributes{nullptr}; + SECURITY_ATTRIBUTES* m_ThreadAttributes{nullptr}; + BOOL m_InheritHandles{false}; + DWORD m_CreationFlags{0UL}; + bool m_Executed{false}; + bool m_StdoutPipe{false}; + + HANDLE m_Stdout{INVALID_HANDLE_VALUE}; +}; + +} // namespace winapi::process + +namespace winapi::file +{ +/** + * @brief internal class to handle file creation (opening) with named + * parameters. + */ +template +class _Create +{ +public: + _Create(const std::basic_string& fileName) : m_FileName(fileName) {} + + _Create& access(DWORD desiredAccess) + { + m_DesiredAccess = desiredAccess; + return *this; + } + + _Create& share(DWORD shareMode) + { + m_ShareMode = shareMode; + return *this; + } + + _Create& createAlways() + { + m_CreationDisposition = CREATE_ALWAYS; + return *this; + } + + _Create& openAlways() + { + m_CreationDisposition = OPEN_ALWAYS; + return *this; + } + + _Create& security(SECURITY_ATTRIBUTES* attributes) + { + m_SecurityAttributes = attributes; + return *this; + } + + _Create& templateFile(HANDLE templateFile) + { + m_Template = templateFile; + return *this; + } + + /// @brief end the named parameter cascade and open the file + HANDLE operator()() + { + return callDelegate( + std::integral_constant()); + } + +private: + HANDLE callDelegate(std::true_type) + { + return ::CreateFileW(m_FileName.c_str(), m_DesiredAccess, m_ShareMode, + m_SecurityAttributes, m_CreationDisposition, m_Flags, + m_Template); + } + + HANDLE callDelegate(std::false_type) + { + return ::CreateFileA(m_FileName.c_str(), m_DesiredAccess, m_ShareMode, + m_SecurityAttributes, m_CreationDisposition, m_Flags, + m_Template); + } + +private: + std::basic_string m_FileName; + DWORD m_DesiredAccess{GENERIC_ALL}; + DWORD m_ShareMode{0UL}; + DWORD m_CreationDisposition{DefaultDisposition}; + DWORD m_Flags{FILE_ATTRIBUTE_NORMAL}; + HANDLE m_Template{nullptr}; + SECURITY_ATTRIBUTES* m_SecurityAttributes{nullptr}; +}; + +} // namespace winapi::file + +namespace winapi::ansi +{ + +std::string getModuleFileName(HMODULE module, HANDLE process = INVALID_HANDLE_VALUE); +std::pair getFullPathName(LPCSTR fileName); +std::string getCurrentDirectory(); +typedef process::_Create createProcess; +typedef file::_Create createFile; +typedef file::_Create openFile; + +} // namespace winapi::ansi + +namespace winapi::wide +{ + +std::wstring getModuleFileName(HMODULE module, HANDLE process = INVALID_HANDLE_VALUE); +std::pair getFullPathName(LPCWSTR fileName); +std::wstring getCurrentDirectory(); +std::wstring getKnownFolderPath(REFKNOWNFOLDERID folderID); + +typedef process::_Create createProcess; +typedef file::_Create createFile; +typedef file::_Create openFile; + +} // namespace winapi::wide + +/** + * useful convenience functions close to the api + */ +namespace winapi::ex +{ + +/** + * @brief retrieve the address range covering the code section of a module + * @param moduleHandle handle to the module + * @return start and end address of the code section + * @note the code section can only be identified if it has the standardized section name + * ".text" Otherwise the whole address range of all sections in the module is returned. + * This happens for compressed exectuables for example + */ +std::pair getSectionRange(HANDLE moduleHandle); + +struct OSVersion +{ + DWORD major; + DWORD minor; + DWORD build; + DWORD platformid; + DWORD servicpack; +}; + +OSVersion getOSVersion(); + +} // namespace winapi::ex + +namespace winapi::ex::ansi +{ + +/** + * @brief retrieve an error string for a windows error message + * @param errorCode the error code to look up. If this is left at the default, + * ::GetLastError is used + * @return string representation of the error. Currently this is localized + */ +std::string errorString(DWORD errorCode = std::numeric_limits::max()); + +/** + * @brief convert filetime to string + * @param time time to convert + * @return a string representation (currently only supports utc and iso format with + * second precision) + */ +std::string toString(const FILETIME& time); + +/** + * @brief find file name in a windows file path + * @param path the path to search in + * @return the file name of the path or an empty string if the path ends on + * a slash + * @note this function doesn't access the file system so it doesn't depend + * on whether the file actually exists. This also means it can't + * determine if a path that doesn't end on a slash refers to a file or + * directory + * @note the return value is a pointer into the same buffer, no copy is + * created + */ +LPCSTR GetBaseName(LPCSTR string); + +} // namespace winapi::ex::ansi + +namespace winapi::ex::wide +{ +/** + * retrieve the name of the binary section containing the specified address + * @param address the address to test + * @param process the process for which to retrieve the section. If this is + * nullptr, the current process is analized. + * @return name of the section or "unknown" if no matching section was found + */ +std::wstring getSectionName(PVOID address, HANDLE process = nullptr); + +/** + * @brief test if a file exists + * @param path path to check + * @param isDirectory (optional) if this isn't null, it will be set to true if the path + * specifies a directory, false otherwise + * @return true if the file (or directory) exists. + */ +bool fileExists(LPCWSTR fileName, bool* isDirectory = nullptr); + +/** + * @brief retrieve an error string for a windows error message + * @param errorCode the error code to look up. If this is left at the default, + * ::GetLastError is used + * @return string representation of the error. Currently this is localized + */ +std::wstring errorString(DWORD errorCode = std::numeric_limits::max()); + +/** + * @brief convert filetime to string + * @param time time to convert + * @return a string representation (currently only supports utc and iso format with + * second precision) + */ +std::wstring toString(const FILETIME& time); + +/** + * @brief find file name in a windows file path + * @param path the path to search in + * @return the file name of the path or an empty string if the path ends on + * a slash + * @note this function doesn't access the file system so it doesn't depend + * on whether the file actually exists. This also means it can't + * determine if a path that doesn't end on a slash refers to a file or + * directory + * @note the return value is a pointer into the same buffer, no copy is + * created + */ +LPCWSTR GetBaseName(LPCWSTR path); + +/** + * @see const-variant of this function + */ +LPWSTR GetBaseName(LPWSTR path); + +struct FileResult +{ + std::wstring fileName; + ULONG attributes; +}; + +/** + * @brief a quick function to find all files in a directory or files following a + * pattern. This uses NtQueryDirectoryFile api internally so it should be faster than + * the usual FindFirstFile/FindNextFile pattern + * @param directoryName name of the directory to search in + * @param pattern name pattern that needs to match + * @return the list of files found + */ +std::vector quickFindFiles(LPCWSTR directoryName, LPCWSTR pattern); + +/** + * @brief create the specified directory including all intermediate + * directories + * @param path the path to create + * @param securityAttributes the security attributes to use for all created + * directories. if this is null (default), the standard attributes + * are used + * @return true if the directory (and possibly parent directories) were actually created + * and false if the directory already existed. Throws exceptions on failure. + */ +bool createPath(boost::filesystem::path path, + LPSECURITY_ATTRIBUTES securityAttributes = nullptr); +inline bool createPath(LPCWSTR path, LPSECURITY_ATTRIBUTES securityAttributes = nullptr) +{ + return createPath(boost::filesystem::path(path), securityAttributes); +} + +std::wstring getWindowsBuildLab(bool ex = false); + +} // namespace winapi::ex::wide + +namespace winapi::process +{ + +template +_Create::_Create(const std::basic_string& binaryName) +{ + if (binaryName.length() > MAX_PATH) { + throw parameter_error("executable filename can't be longer than 260 characters"); + } + m_CommandLine << "\"" << binaryName << "\""; +} + +template +_Create& _Create::workingDirectory(const std::basic_string& path) +{ + m_CurrentDirectory = path; + return *this; +} + +template +_Create& _Create::processAttributes(SECURITY_ATTRIBUTES* attributes) +{ + m_ProcessAttributes = attributes; + return *this; +} + +template +_Create& _Create::threadAttributes(SECURITY_ATTRIBUTES* attributes) +{ + m_ThreadAttributes = attributes; + return *this; +} + +template +_Create& _Create::inheritHandles() +{ + m_InheritHandles = true; + return *this; +} + +template +_Create& _Create::suspended() +{ + m_CreationFlags |= CREATE_SUSPENDED; + return *this; +} + +template +_Create& _Create::stdoutPipe() +{ + m_StdoutPipe = true; + return *this; +} + +} // namespace winapi::process diff --git a/libs/usvfs/src/shared/windows_sane.h b/libs/usvfs/src/shared/windows_sane.h new file mode 100644 index 0000000..7bb3726 --- /dev/null +++ b/libs/usvfs/src/shared/windows_sane.h @@ -0,0 +1,28 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#define WIN32_LEAN_AND_MEAN +#include diff --git a/libs/usvfs/src/thooklib/CMakeLists.txt b/libs/usvfs/src/thooklib/CMakeLists.txt new file mode 100644 index 0000000..48426db --- /dev/null +++ b/libs/usvfs/src/thooklib/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(asmjit CONFIG REQUIRED) +find_library(LIBUDIS86_LIBRARY libudis86) +find_package(spdlog CONFIG REQUIRED) + +file(GLOB sources CONFIGURE_DEPENDS "*.cpp" "*.h") +source_group("" FILES ${sources}) + +add_library(thooklib STATIC ${sources}) +target_link_libraries(thooklib PRIVATE shared asmjit::asmjit ${LIBUDIS86_LIBRARY} spdlog::spdlog_header_only) +target_include_directories(thooklib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +set_target_properties(thooklib PROPERTIES FOLDER injection) +target_precompile_headers(shared PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../shared/pch.h) diff --git a/libs/usvfs/src/thooklib/asmjit_sane.h b/libs/usvfs/src/thooklib/asmjit_sane.h new file mode 100644 index 0000000..365622a --- /dev/null +++ b/libs/usvfs/src/thooklib/asmjit_sane.h @@ -0,0 +1,29 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#pragma warning(push) +#pragma warning(disable : 4201) +#pragma warning(disable : 4244) +#pragma warning(disable : 4245) +#include +#include +#pragma warning(pop) diff --git a/libs/usvfs/src/thooklib/hooklib.cpp b/libs/usvfs/src/thooklib/hooklib.cpp new file mode 100644 index 0000000..9dbbafa --- /dev/null +++ b/libs/usvfs/src/thooklib/hooklib.cpp @@ -0,0 +1,740 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "udis86wrapper.h" +#include +#include +#include +#pragma warning(push, 3) +#include +#pragma warning(pop) +#include "hooklib.h" +#include "ttrampolinepool.h" +#include "utility.h" +#include +#include +#include +#include + +#if BOOST_ARCH_X86_64 +#pragma message("64bit build") +#define JUMP_SIZE 13 +#elif BOOST_ARCH_X86_32 +#define JUMP_SIZE 5 +#else +#error "unsupported architecture" +#endif + +using namespace asmjit; +// from here on out I'll only test for 64 or "other" + +using namespace HookLib; +using namespace asmjit; + +using namespace usvfs; + +struct THookInfo +{ + LPVOID originalFunction; + LPVOID replacementFunction; + LPVOID detour; // detour to call the original function after hook was installed. + LPVOID trampoline; // code fragment that decides whether the replacement function or + // detour is executed (preventing endless loops) + std::vector + preamble; // part of the detour that needs to be re-inserted into the original + // function to return it to vanilla state + bool stub; // if this is true, the trampoline calls the "replacement"-function that + // before the original function, not instead of it + enum + { + TYPE_HOTPATCH, // official hot-patch variant as used on 32-bit windows + TYPE_WIN64PATCH, // custom patch variant used on 64-bit windows + TYPE_CHAINPATCH, // the hook is part of the hook chain (and not the first) + TYPE_OVERWRITE, // full jump overwrite used if none of the above work + TYPE_RIPINDIRECT // the function already started on a rip-relative jump so we only + // modified that variable + } type; +}; + +UDis86Wrapper& disasm() +{ + static UDis86Wrapper instance; + return instance; +} + +void PauseOtherThreads() +{ + // TODO: implement me! +} + +void ResumePausedThreads() +{ + // TODO: implement me! should resume only the threads paused by PauseOtherThreads +} + +// not using the disassembler because this is simpler +LPBYTE ShortJumpTarget(LPBYTE address) +{ + int8_t off = *(address + 1); + return address + 2 + off; +} + +size_t GetJumpSize(LPBYTE, LPVOID) +{ + // TODO: it would be neater to use asmjit to generate this jump and ask asmjit for + // the size of this jump but with asmjit I can only generate absolute jumps, which + // take too much space. + + // Since trampoline buffers is always allocated within 32-bit + // address range of jump, we can say with confidence that a 5-byte jump is possible + return 5; +} + +void WriteLongJump(LPBYTE jumpAddr, LPVOID destination) +{ + // TODO: not using asmjit here because I couldn't figure out how to generate + // a working, space-optimized, relative jump to outside the generated code and + // we do want to optimize this jump +#if BOOST_ARCH_X86_64 + intptr_t dist = reinterpret_cast(destination) - + (reinterpret_cast(jumpAddr) + 5); + int32_t distShort = static_cast(dist); +#else + int32_t distShort = reinterpret_cast(destination) - + (reinterpret_cast(jumpAddr) + 5); +#endif + *jumpAddr = 0xE9; + *reinterpret_cast(jumpAddr + 1) = distShort; +} + +void WriteSingleJump(THookInfo& hookInfo, HookError* error) +{ + DWORD oldprotect, ignore; + // Set the target function to copy on write, so we don't modify code for other + // processes + if (!VirtualProtect(hookInfo.originalFunction, JUMP_SIZE, PAGE_EXECUTE_WRITECOPY, + &oldprotect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + WriteLongJump(reinterpret_cast(hookInfo.originalFunction), + hookInfo.trampoline); + + // restore old memory protection + if (!VirtualProtect(hookInfo.originalFunction, JUMP_SIZE, oldprotect, &ignore)) { + throw std::runtime_error("failed to change virtual protection"); + } + + if (error != nullptr) { + *error = ERR_NONE; + } +} + +void WriteShortJump(LPBYTE jumpAddr, const signed char offset) +{ + *jumpAddr = 0xEB; + *(jumpAddr + 1) = offset; +} + +void WriteIndirectJump(THookInfo& hookInfo, size_t jumpSize, HookError* error) +{ + DWORD oldProtect = 0; + LPBYTE jumpAddr = reinterpret_cast(hookInfo.originalFunction) - jumpSize; + // allow write to jump address + the short jump inside the function + if (!VirtualProtect(jumpAddr, jumpSize + 2, PAGE_EXECUTE_WRITECOPY, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + // insert the long jump first, then the short jump to the long jump, thus + // activating the reroute + WriteLongJump(jumpAddr, hookInfo.trampoline); + + PauseOtherThreads(); + WriteShortJump(jumpAddr + jumpSize, -(static_cast(jumpSize) + 2)); + ResumePausedThreads(); + + // restore access protection + if (!VirtualProtect(jumpAddr, jumpSize + 2, oldProtect, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + if (error != nullptr) { + *error = ERR_NONE; + } +} + +/// implements function hooking using the mechanism intended for hot patching +/// Explanation: the visual studio compiler offers an option to prepare functions +/// for hot patching. In this case the compiler leaves room for one far jump before +/// the actual function and a 2-byte nop inside the function. +/// to hook such a function we write a jump to our replacement function to the +/// space before the function and short jump to that jump to where the 2-byte nop +/// was. +/// On 32-bit windows, MS seems to have compiled all relevant functions in the +/// windows API for hot patching starting with Windows XP SP3 +/// On 64-bit windows a lot of functions have the space for a jump before the function +/// but they don't have the 2-byte nop so this function doesn't work +/// \param hookInfo info about the hook to be installed +/// \return true on success, false on error +BOOL HookHotPatch(THookInfo& hookInfo, HookError* error) +{ + LPVOID original = reinterpret_cast(hookInfo.originalFunction); + + if (hookInfo.stub) { + hookInfo.trampoline = TrampolinePool::instance().storeStub( + hookInfo.replacementFunction, + reinterpret_cast(hookInfo.originalFunction), + shared::AddrAdd(original, 2)); + } else { + hookInfo.trampoline = TrampolinePool::instance().storeTrampoline( + hookInfo.replacementFunction, + reinterpret_cast(hookInfo.originalFunction), + shared::AddrAdd(original, 2)); + } + + WriteIndirectJump(hookInfo, JUMP_SIZE, error); + hookInfo.type = THookInfo::TYPE_HOTPATCH; + // in this case we don't need a separate detour, we simply jump past the 2-byte nop + hookInfo.detour = reinterpret_cast(hookInfo.originalFunction) + 2; + + return TRUE; +} + +uintptr_t followJumps(THookInfo& hookInfo) +{ + LPBYTE original = reinterpret_cast(hookInfo.originalFunction); + LPBYTE shortTarget = ShortJumpTarget(original); + + // disassemble the long jump + disasm().setInputBuffer(shortTarget, JUMP_SIZE); + + ud_disassemble(disasm()); + if (ud_insn_mnemonic(disasm()) != UD_Ijmp) { + // this shouldn't happen, we only call this if the jump was discovered before + throw std::runtime_error("failed to find jump in patch"); + } + + uint64_t res = ud_insn_off(disasm()) + ud_insn_len(disasm()); + res += disasm().jumpOffset(); + + return static_cast(res); +} + +/// +/// \brief hook a call that is implemented as a short-jump to a long jump using a +/// rip-relative address variable +/// \param hookInfo info about the hook to be installed +/// \param error if the return code is false and this is not null, the referred-to +/// variable is set to an error code +/// \return true on success, false on error +/// +BOOL HookRIPIndirection(THookInfo& hookInfo, HookError* error) +{ + uintptr_t res = followJumps(hookInfo); + + const ud_operand_t* op = ud_insn_opr(disasm(), 0); + + if (op->base != UD_R_RIP) { + throw std::runtime_error("expected rip-relative addressing"); + } + + auto chainNext = disasm().jumpTarget(); + if (hookInfo.stub) { + hookInfo.trampoline = TrampolinePool::instance().storeStub( + hookInfo.replacementFunction, + reinterpret_cast(hookInfo.originalFunction), + reinterpret_cast(chainNext)); + } else { + hookInfo.trampoline = TrampolinePool::instance().storeTrampoline( + hookInfo.replacementFunction, + reinterpret_cast(hookInfo.originalFunction), + reinterpret_cast(chainNext)); + } + + DWORD oldProtect = 0; + if (!VirtualProtect(reinterpret_cast(res), 2, PAGE_EXECUTE_WRITECOPY, + &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + *reinterpret_cast(res) = reinterpret_cast(hookInfo.trampoline); + + if (!VirtualProtect(reinterpret_cast(res), 2, oldProtect, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + hookInfo.type = THookInfo::TYPE_RIPINDIRECT; + hookInfo.detour = reinterpret_cast(chainNext); + + if (error != nullptr) { + *error = ERR_NONE; + } + + return TRUE; +} + +BOOL HookChainHook(THookInfo& hookInfo, LPBYTE jumpPos, HookError* error) +{ + // disassemble the long jump + disasm().setInputBuffer(jumpPos, JUMP_SIZE); + + ud_disassemble(disasm()); + if (ud_insn_mnemonic(disasm()) != UD_Ijmp) { + // this shouldn't happen, we only call this if the jump was discovered before + throw std::runtime_error("failed to find jump in patch"); + } + + auto chainTarget = disasm().jumpTarget(); + + size_t size = ud_insn_len(disasm()); + + // save the original code for the preamble so we can restore it later + hookInfo.preamble.resize(size); + memcpy(&hookInfo.preamble[0], jumpPos, size); + + spdlog::get("usvfs")->info("existing hook to {0:x} in {1}", chainTarget, + shared::string_cast( + winapi::ex::wide::getSectionName((void*)chainTarget))); + + if (hookInfo.stub) { + hookInfo.trampoline = TrampolinePool::instance().storeStub( + hookInfo.replacementFunction, + reinterpret_cast(hookInfo.originalFunction), + reinterpret_cast(chainTarget)); + } else { + hookInfo.trampoline = TrampolinePool::instance().storeTrampoline( + hookInfo.replacementFunction, + reinterpret_cast(hookInfo.originalFunction), + reinterpret_cast(chainTarget)); + } + + DWORD oldProtect = 0; + if (!VirtualProtect(jumpPos, size, PAGE_EXECUTE_WRITECOPY, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + WriteLongJump(jumpPos, hookInfo.trampoline); + + if (!VirtualProtect(jumpPos, size, oldProtect, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + hookInfo.type = THookInfo::TYPE_CHAINPATCH; + hookInfo.detour = reinterpret_cast(chainTarget); + + if (error != nullptr) { + *error = ERR_NONE; + } + + return TRUE; +} + +/// +/// implements hooking by chaining to an existing hook +/// \param hookInfo info about the hook to be installed +/// \param error if the return code is false and this is not null, +/// the referred-to variable is set to an error code +/// \return true on success, false on error +/// +BOOL HookChainHook(THookInfo& hookInfo, HookError* error) +{ + return HookChainHook(hookInfo, reinterpret_cast(hookInfo.originalFunction), + error); +} + +/// +/// implements hooking by chaining to an existing hot patch +/// \param hookInfo info about the hook to be installed +/// \param error if the return code is false and this is not null, +/// the referred-to variable is set to an error code +/// \return true on success, false on error +/// +BOOL HookChainPatch(THookInfo& hookInfo, HookError* error) +{ + LPBYTE original = reinterpret_cast(hookInfo.originalFunction); + LPBYTE shortTarget = ShortJumpTarget(original); + + return HookChainHook(hookInfo, shortTarget, error); +} + +/// implements function hooking by overwriting the first n bytes of the function +/// with a jump to the replacement function. Since this is destructive to the original +/// function code the first n bytes of the function need to be copied somewhere else +/// and that code needs to be called via a detour. This is a lot more complex than +/// the hotpatch mechanism. +/// \param hookInfo info about the hook to be installed +/// \param error if the return code is false and this is not null, the referred-to +/// variable is set to an error code +/// \return true on success, false on error +BOOL HookDisasm(THookInfo& hookInfo, HookError* error) +{ + LPBYTE address = reinterpret_cast(hookInfo.originalFunction); + ud_set_input_buffer(disasm(), address, 40); + + size_t jumpSize = GetJumpSize( + static_cast(hookInfo.originalFunction), + TrampolinePool::instance().currentBufferAddress(hookInfo.originalFunction)); + + // test if we have room for a jump before the function + bool jumpspace = true; + for (size_t i = 0; i < jumpSize; ++i) { + if (*(address - i - 1) != 0x90) { + jumpspace = false; + break; + } + } + + size_t minSize = jumpspace ? 2 : jumpSize; + + // iterate over all instructions that overlap with the jump instructions + // we want to write. + // TODO right now, this does not test if the function is smaller than the jump. + size_t size = 0; + while (size < minSize) { + if (ud_disassemble(disasm()) == 0) { + throw std::runtime_error("premature end of file in disassembly"); + } + + if ((size == 0) && (ud_insn_mnemonic(disasm()) == UD_Ijmp)) { + if (error != nullptr) { + *error = ERR_JUMP; + } + return FALSE; + } + + size += ud_insn_len(disasm()); + + // ret instruction or int3 smells like function end + if ((ud_insn_mnemonic(disasm()) == UD_Iret) || + (ud_insn_mnemonic(disasm()) == UD_Iint3)) { + if (error != nullptr) { + *error = ERR_FUNCEND; + } + return FALSE; + } + + // check the operands for relative addressing (not supported) + for (int i = 0; i < 3; ++i) { + const ud_operand* op = ud_insn_opr(disasm(), i); + if (op) { + if ( + // rip-relative are not handled, usually relative jumps + op->base == UD_R_RIP + + // rsp-relative call are not handled (there are valid rsp-relative + // instructions, e.g. sub) + || (ud_insn_mnemonic(disasm()) == UD_Icall && op->base == UD_R_RSP)) { + if (error != nullptr) { + *error = ERR_RIP; + } + return FALSE; + } + } + } + } + + // save the original code for the preamble so we can restore it later + hookInfo.preamble.resize(size); + memcpy(&hookInfo.preamble[0], hookInfo.originalFunction, size); + + size_t rerouteOffset = 0; + if (hookInfo.stub) { + hookInfo.trampoline = TrampolinePool::instance().storeStub( + hookInfo.replacementFunction, hookInfo.originalFunction, size, &rerouteOffset); + } else { + hookInfo.trampoline = TrampolinePool::instance().storeTrampoline( + hookInfo.replacementFunction, hookInfo.originalFunction, size, &rerouteOffset); + } + + if (jumpspace) { + WriteIndirectJump(hookInfo, jumpSize, error); + hookInfo.type = THookInfo::TYPE_WIN64PATCH; + } else { + WriteSingleJump(hookInfo, error); + hookInfo.type = THookInfo::TYPE_OVERWRITE; + } + hookInfo.detour = reinterpret_cast(hookInfo.trampoline) + rerouteOffset; + + return TRUE; +} + +enum EPreamble +{ + PRE_PATCHFREE, + PRE_PATCHUSED, + PRE_RIPINDIRECT, + PRE_FOREIGNHOOK, + PRE_UNKNOWN +}; + +EPreamble DeterminePreamble(LPBYTE address) +{ + ud_set_input_buffer(disasm(), address, JUMP_SIZE); + ud_disassemble(disasm()); + + if ((ud_insn_mnemonic(disasm()) == UD_Imov) && + (ud_insn_opr(disasm(), 0) == ud_insn_opr(disasm(), 1)) && + (ud_insn_opr(disasm(), 0)->type == UD_OP_REG)) { + // mov edi, edi + return PRE_PATCHFREE; + } else if ((ud_insn_mnemonic(disasm()) == UD_Ijmp) && (ud_insn_len(disasm()) == 2)) { + // determine target of the short jump + LPBYTE shortTarget = ShortJumpTarget(address); + + // test if that short jump leads to a long jump + ud_set_input_buffer(disasm(), shortTarget, JUMP_SIZE); + ud_disassemble(disasm()); + if (ud_insn_mnemonic(disasm()) == UD_Ijmp) { + const ud_operand* op = ud_insn_opr(disasm(), 0); + if (op->base == UD_R_RIP) { + return PRE_RIPINDIRECT; + } else { + return PRE_PATCHUSED; + } + } else { + return PRE_UNKNOWN; + } + } else if (ud_insn_mnemonic(disasm()) == UD_Ijmp) { + return PRE_FOREIGNHOOK; + } else { + return PRE_UNKNOWN; + } +} + +static std::map s_Hooks; + +static HOOKHANDLE GenerateHandle() +{ + static ULONG NextHandle = 1; + return NextHandle++; +} + +HOOKHANDLE applyHook(THookInfo info, HookError* error) +{ + // apply the correct hook function depending on how the function start looks + EPreamble preamble = DeterminePreamble((LPBYTE)info.originalFunction); + + BOOL success = FALSE; + switch (preamble) { + case PRE_PATCHUSED: { + success = HookChainPatch(info, error); + } break; + case PRE_PATCHFREE: { + success = HookHotPatch(info, error); + } break; + case PRE_RIPINDIRECT: { + success = HookRIPIndirection(info, error); + } break; + case PRE_FOREIGNHOOK: { + success = HookChainHook(info, error); + } break; + default: { + success = HookDisasm(info, error); + } break; + } + + if (success == TRUE) { + HOOKHANDLE handle = GenerateHandle(); + s_Hooks[handle] = info; + return handle; + } else { + return INVALID_HOOK; + } +} + +HOOKHANDLE HookLib::InstallStub(LPVOID functionAddress, LPVOID stubAddress, + HookError* error) +{ + if (functionAddress == nullptr) { + if (error != nullptr) + *error = ERR_INVALIDPARAMETERS; + return INVALID_HOOK; + } + + THookInfo info; + info.originalFunction = functionAddress; + info.replacementFunction = stubAddress; + info.stub = true; + info.detour = nullptr; + info.trampoline = nullptr; + info.type = THookInfo::TYPE_OVERWRITE; + + return applyHook(info, error); +} + +HOOKHANDLE HookLib::InstallStub(HMODULE module, LPCSTR functionName, LPVOID stubAddress, + HookError* error) +{ + LPVOID funcAddr = MyGetProcAddress(module, functionName); + return InstallStub(funcAddr, stubAddress, error); +} + +HOOKHANDLE HookLib::InstallHook(LPVOID functionAddress, LPVOID hookAddress, + HookError* error) +{ + if (functionAddress == nullptr) { + if (error != nullptr) + *error = ERR_INVALIDPARAMETERS; + return INVALID_HOOK; + } + THookInfo info; + info.originalFunction = functionAddress; + info.replacementFunction = hookAddress; + info.stub = false; + info.detour = nullptr; + info.trampoline = nullptr; + info.type = THookInfo::TYPE_OVERWRITE; + + return applyHook(info, error); +} + +HOOKHANDLE HookLib::InstallHook(HMODULE module, LPCSTR functionName, LPVOID hookAddress, + HookError* error) +{ + LPVOID funcAddr = MyGetProcAddress(module, functionName); + return InstallHook(funcAddr, hookAddress, error); +} + +void HookLib::RemoveHook(HOOKHANDLE handle) +{ + auto iter = s_Hooks.find(handle); + if (iter != s_Hooks.end()) { + THookInfo info = iter->second; + PauseOtherThreads(); + LPBYTE address = reinterpret_cast(info.originalFunction); + if (info.type == THookInfo::TYPE_HOTPATCH) { + // return the short jump to 2-byte nop + // TODO: This doesn't take into account if we chain-loaded another hook + + DWORD oldProtect = 0; + if (!VirtualProtect(address, 2, PAGE_EXECUTE_WRITECOPY, &oldProtect)) { + throw shared::windows_error("failed to gain write access to remove hook"); + } + *address = 0x8b; + *(address + 1) = 0xff; + VirtualProtect(address, 2, oldProtect, &oldProtect); + } else if ((info.type == THookInfo::TYPE_OVERWRITE) || + (info.type == THookInfo::TYPE_WIN64PATCH)) { + DWORD oldProtect = 0; + if (!VirtualProtect(address, info.preamble.size(), PAGE_EXECUTE_WRITECOPY, + &oldProtect)) { + throw shared::windows_error("failed to gain write access to remove hook"); + } + // TODO: remove hook by restoring the original function. This only works if we + // have the exact code available somewhere + memcpy(address, &info.preamble[0], info.preamble.size()); + VirtualProtect(address, info.preamble.size(), oldProtect, &oldProtect); + } else if (info.type == THookInfo::TYPE_CHAINPATCH) { + // we could attempt to restore the original function preamble but I'm not + // sure we can reliably write the jump with same (or lower) size. + // Instead overwrite our own trampoline + disasm().setInputBuffer(static_cast(info.originalFunction), JUMP_SIZE); + ud_disassemble(disasm()); + if (ud_insn_mnemonic(disasm()) != UD_Ijmp) { + // this shouldn't happen, we only call this if the jump was discovered before + throw std::runtime_error("failed to find jump in patch"); + } + + LPBYTE jumpPos = static_cast(info.originalFunction); + if (ud_insn_len(disasm()) == 2) { + jumpPos = reinterpret_cast(disasm().jumpTarget()); + } + + DWORD oldProtect = 0; + if (!VirtualProtect(jumpPos, info.preamble.size(), PAGE_EXECUTE_WRITECOPY, + &oldProtect)) { + throw shared::windows_error("failed to gain write access to remove hook"); + } + memcpy(jumpPos, info.preamble.data(), info.preamble.size()); + VirtualProtect(jumpPos, info.preamble.size(), oldProtect, &oldProtect); + } else if (info.type == THookInfo::TYPE_RIPINDIRECT) { + uintptr_t res = followJumps(info); + DWORD oldProtect = 0; + if (!VirtualProtect(reinterpret_cast(res), JUMP_SIZE, + PAGE_EXECUTE_WRITECOPY, &oldProtect)) { + throw shared::windows_error("failed to gain write access to remove hook"); + } + *reinterpret_cast(res) = + reinterpret_cast(info.originalFunction); + VirtualProtect(reinterpret_cast(res), JUMP_SIZE, oldProtect, &oldProtect); + } else { + spdlog::get("usvfs")->critical("can't remove hook, unknown hook type!"); + } + + s_Hooks.erase(iter); + ResumePausedThreads(); + } else { + spdlog::get("usvfs")->info("handle unknown: {0:x}", handle); + } +} + +const char* HookLib::GetErrorString(HookError err) +{ + switch (err) { + case ERR_NONE: + return "No Error"; + case ERR_INVALIDPARAMETERS: + return "Invalid parameters"; + case ERR_FUNCEND: + return "Function too short"; + case ERR_JUMP: + return "Function starts on a jump"; + case ERR_RIP: + return "RIP-relative addressing can't be relocated."; + case ERR_RELJUMP: + return "Relative Jump can't be relocated."; + default: + return "Unkown error code"; + } +} + +const char* HookLib::GetHookType(HOOKHANDLE handle) +{ + auto iter = s_Hooks.find(handle); + if (iter != s_Hooks.end()) { + THookInfo info = iter->second; + switch (info.type) { + case THookInfo::TYPE_HOTPATCH: + return "hot patch"; + case THookInfo::TYPE_WIN64PATCH: + return "64-bit hot patch"; + case THookInfo::TYPE_CHAINPATCH: + return "chained patch"; + case THookInfo::TYPE_OVERWRITE: + return "overwrite"; + case THookInfo::TYPE_RIPINDIRECT: + return "rip indirection modified"; + default: { + spdlog::get("usvfs")->error("invalid hook type {0}", info.type); + return "invalid hook type"; + } + } + } + return "invalid handle"; +} + +LPVOID HookLib::GetDetour(HOOKHANDLE handle) +{ + auto iter = s_Hooks.find(handle); + if (iter != s_Hooks.end()) { + THookInfo info = iter->second; + return info.detour; + } + return nullptr; +} diff --git a/libs/usvfs/src/thooklib/hooklib.h b/libs/usvfs/src/thooklib/hooklib.h new file mode 100644 index 0000000..a549c03 --- /dev/null +++ b/libs/usvfs/src/thooklib/hooklib.h @@ -0,0 +1,125 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include +#include + +namespace HookLib +{ + +enum HookError +{ + ERR_NONE, + ERR_INVALIDPARAMETERS, // parameters are invalid + ERR_FUNCEND, // function is too short to be hooked + ERR_JUMP, // function consists only of an unconditional jump. Maybe it has already + // been hooked? + ERR_RIP, // segment of the function to be overwritten contains a instruction-relative + // operation + ERR_RELJUMP // segment of the function to be overwritten contains a relative jump we + // can't relocated +}; + +typedef ULONG HOOKHANDLE; +static const HOOKHANDLE INVALID_HOOK = (HOOKHANDLE)-1; + +/// +/// \brief install a stub (function to be called before the target function) +/// \param functionAddress address of the function to stub +/// \param stubAddress address of the stub function. This function has to have the +/// signature of void foobar(LPVOID address). +/// address receives the address of the function. +/// \param error (optional) if set, the referenced variable will receive an error code +/// describing the problem (if any) +/// \return a handle to reference the hook in later operations or INVALID_HOOK on error +/// +HOOKHANDLE InstallStub(LPVOID functionAddress, LPVOID stubAddress, + HookError* error = nullptr); + +/// +/// \brief install a stub (function to be called before the target function) +/// \param module the module containing the function to hook +/// \param functionName name of the function to stub (as exported by the library) +/// \param stubAddress address of the stub function. This function has to have the +/// signature of void foobar(LPVOID address). +/// address receives the address of the function. +/// \param error (optional) if set, the referenced variable will receive an error code +/// describing the problem (if any) +/// \return a handle to reference the hook in later operations or INVALID_HOOK on error +/// +HOOKHANDLE InstallStub(HMODULE module, LPCSTR functionName, LPVOID stubAddress, + HookError* error = nullptr); + +/// +/// \brief install a hook (function replacing the existing functionality of the +/// function) +/// \param functionAddress address of the function to hook +/// \param hookAddress address of the replacement function. This function has to have +/// the exact same signature as the replaced function +/// \param error (optional) if set, the referenced variable will receive an error code +/// describing the problem (if any) +/// \return a handle to reference the hook in later operations or INVALID_HOOK on error +/// +HOOKHANDLE InstallHook(LPVOID functionAddress, LPVOID hookAddress, + HookError* error = nullptr); + +/// +/// \brief install a hook (function replacing the existing functionality of the +/// function) +/// \param functionName name of the function to hook (as exported by the library) +/// \param hookAddress address of the replacement function. This function has to have +/// the exact same signature as the replaced function +/// \param error (optional) if set, the referenced variable will receive an error code +/// describing the problem (if any) +/// \return a handle to reference the hook in later operations or INVALID_HOOK on error +/// +HOOKHANDLE InstallHook(HMODULE module, LPCSTR functionName, LPVOID hookAddress, + HookError* error = nullptr); + +/// +/// \brief remove a hook +/// \param handle handle returned in InstallStub or InstallHook +/// +void RemoveHook(HOOKHANDLE handle); + +/// +/// \brief determine the type of a hook +/// \param handle the handle to look up +/// \return a string describing the used hooking mechanism +/// +const char* GetHookType(HOOKHANDLE handle); + +/// +/// \brief retrieve the address that can be used to directly call a detour +/// \param handle handle for the hook +/// \return function address +/// +LPVOID GetDetour(HOOKHANDLE handle); + +/// +/// \brief resolve an error code to a descriptive string +/// \param err the error code to resolve +/// \return the error string +/// +const char* GetErrorString(HookError err); + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/pch.cpp b/libs/usvfs/src/thooklib/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/libs/usvfs/src/thooklib/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/libs/usvfs/src/thooklib/ttrampolinepool.cpp b/libs/usvfs/src/thooklib/ttrampolinepool.cpp new file mode 100644 index 0000000..3b6a51f --- /dev/null +++ b/libs/usvfs/src/thooklib/ttrampolinepool.cpp @@ -0,0 +1,601 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "ttrampolinepool.h" +#include +#include +// #include +#include "udis86wrapper.h" + +using namespace asmjit; +#if BOOST_ARCH_X86_64 +using namespace x86; +#elif BOOST_ARCH_X86_32 +using namespace asmjit::x86; +#endif + +using namespace usvfs::shared; + +namespace HookLib +{ + +TrampolinePool* TrampolinePool::s_Instance = nullptr; + +TrampolinePool::TrampolinePool() : m_MaxTrampolineSize(sizeof(LPVOID)) +{ + m_BarrierAddr = &TrampolinePool::barrier; + m_ReleaseAddr = &TrampolinePool::release; + + SYSTEM_INFO sysInfo; + ::ZeroMemory(&sysInfo, sizeof(SYSTEM_INFO)); + GetSystemInfo(&sysInfo); + m_BufferSize = sysInfo.dwPageSize; + + // if search range = ffffff then addressmask = ffffffffff000000 + // => all jumps between xxxxxxxxxx000000 and xxxxxxxxxxffffff will use the same buffer + // for trampolines which is guaranteed to be in that range + // TODO it should be valid to use 2 ^ 32 as the search range to increase our chances + // of finding a memory block we can reserve but then there is a problem with + // converting negative jump distances to 32 bit I didn't understand. Everything + // up to 2 ^ 31 seems to be fine though + m_SearchRange = static_cast(pow(2, 30)) - 1; + m_AddressMask = std::numeric_limits::max() - m_SearchRange; +} + +// static +void TrampolinePool::initialize() +{ + if (!s_Instance) + s_Instance = new TrampolinePool(); +} + +void TrampolinePool::setBlock(bool block) +{ + m_FullBlock = block; + if (m_ThreadGuards.get() == nullptr) { + m_ThreadGuards.reset(new TThreadMap()); + } +} + +#if BOOST_ARCH_X86_64 +// push all registers (except rax) and flags to the stack +static void pushAll(X86Assembler& assembler) +{ + assembler.pushf(); + assembler.push(rcx); + assembler.push(rdx); + assembler.push(rbx); + assembler.push(rbp); + assembler.push(rsi); + assembler.push(rdi); + assembler.push(r8); + assembler.push(r9); + assembler.push(r10); + assembler.push(r11); + assembler.push(r12); + assembler.push(r13); + assembler.push(r14); + assembler.push(r15); +} + +// pop all registers (except rax) and flags from stack +static void popAll(X86Assembler& assembler) +{ + assembler.pop(r15); + assembler.pop(r14); + assembler.pop(r13); + assembler.pop(r12); + assembler.pop(r11); + assembler.pop(r10); + assembler.pop(r9); + assembler.pop(r8); + assembler.pop(rdi); + assembler.pop(rsi); + assembler.pop(rbp); + assembler.pop(rbx); + assembler.pop(rdx); + assembler.pop(rcx); + assembler.popf(); +} +#endif // BOOST_ARCH_X86_64 + +void TrampolinePool::addBarrier(LPVOID rerouteAddr, LPVOID original, + X86Assembler& assembler) +{ + Label skipLabel = assembler.newLabel(); + +#if BOOST_ARCH_X86_64 + pushAll(assembler); + assembler.mov(rcx, + imm(reinterpret_cast( + original))); // set call parameter for call to barrier function + assembler.mov(rax, imm((intptr_t)(void*)barrier)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + popAll(assembler); + // test barrier + assembler.cmp(rax, 0); // test if the barrier is locked + assembler.jz(skipLabel); // skip if barrier was locked + + // call replacement function + // for this call no registers are saved. the called function is a compiled function so + // it should correctly save non-volatile registers, and the caller can't expect the + // volatile ones to remain valid + assembler.pop(r10); + assembler.mov(dword_ptr(rax), r10); // store that return address to the variable + // supplied by the barrier function + assembler.mov(rax, imm((intptr_t)(LPVOID)rerouteAddr)); + assembler.call(rax); + assembler.push(rax); // save away result + + // open the barrier again + pushAll(assembler); + assembler.mov(rcx, imm(reinterpret_cast(original))); + assembler.mov(rax, imm((intptr_t)(void*)release)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + popAll(assembler); + assembler.pop(r10); // get the result from the replacement function to a register + assembler.push(rax); // push the original return address back on the stack + assembler.mov(rax, r10); // move result of actual call to rax + assembler.ret(); // return, using the original return address +#else // BOOST_ARCH_X86_64 + assembler.push(imm(void_ptr_cast( + original))); // push original function, as parameter to barrier + assembler.mov(ecx, (Ptr) static_cast(TrampolinePool::barrier)); + assembler.call(ecx); // call barrier function + assembler.cmp(eax, 0); + assembler.jz(skipLabel); // if barrier is locked, jump to end of function + + // case a: we got through the barrier + assembler.pop(ecx); // pop the return address into ecx + assembler.mov(dword_ptr(eax), ecx); // store that return address in the variable + // supplied by the barrier function + + assembler.mov(eax, (Ptr) static_cast(rerouteAddr)); + assembler.call(eax); // call replacement function (pointer was stored in front of the + // trampoline) (this function gets the parameters that were on + // the stack already and cleans + // them up itself (stdcall convention)) + assembler.push(eax); // save away result + assembler.push(imm(void_ptr_cast(original))); // open the barrier again + assembler.mov(eax, (Ptr) static_cast(TrampolinePool::release)); + assembler.call(eax); + assembler.pop(ecx); // pop the result from the actual call to ecx + assembler.push(eax); // push the original return address (returned by + // TTrampolinePool::release) back on the stack + assembler.mov(eax, ecx); // move result of actual call to eax + assembler.ret(); // return, using the original return address +#endif // BOOST_ARCH_X86_64 + + assembler.bind(skipLabel); +} + +LPVOID TrampolinePool::roundAddress(LPVOID address) const +{ + return reinterpret_cast(reinterpret_cast(address) & m_AddressMask); +} + +TrampolinePool::BufferList& TrampolinePool::getBufferList(LPVOID address) +{ + LPVOID rounded = roundAddress(address); + auto iter = m_Buffers.find(rounded); + if (iter == m_Buffers.end()) { + BufferList newBufList = {0, std::vector()}; + m_Buffers[rounded] = newBufList; + iter = allocateBuffer(address); + } + return iter->second; +} + +LPVOID TrampolinePool::storeStub(LPVOID reroute, LPVOID original, LPVOID returnAddress) +{ + BufferList& bufferList = getBufferList(original); + // first test to increase likelyhood we don't have to reallocate later + if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) { + allocateBuffer(original); + } + + LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset); + + // ??? write address of reroute to trampoline and move past the address + *reinterpret_cast(spot) = reroute; + // coverity[suspicious_sizeof] + spot = AddrAdd(spot, sizeof(LPVOID)); + bufferList.offset += sizeof(LPVOID); + + JitRuntime runtime; +#if BOOST_ARCH_X86_64 + X86Assembler assembler(&runtime); +#else + X86Assembler assembler(&runtime); +#endif + addCallToStub(assembler, original, reroute); + addAbsoluteJump(assembler, reinterpret_cast(returnAddress)); + + size_t codeSize = assembler.getCodeSize(); + + m_MaxTrampolineSize = + std::max(m_MaxTrampolineSize, static_cast(codeSize + sizeof(LPVOID))); + + // final test to see if we can store the trampoline in the buffer + if ((bufferList.offset + codeSize) > m_BufferSize) { + // can't place function in buffer, allocate another and try again + allocateBuffer(original); + // we could relocate the code and the data but this is simpler + return storeStub(reroute, original, returnAddress); + } + + // adjust relative jumps for move to buffer + codeSize = assembler.relocCode(spot); + + uint8_t* code = assembler.getBuffer(); + memcpy(spot, code, codeSize); + + bufferList.offset += codeSize; + + return spot; +} + +LPVOID TrampolinePool::storeTrampoline(LPVOID reroute, LPVOID original, + LPVOID returnAddress) +{ + BufferList& bufferList = getBufferList(original); + // first test to increase likelyhood we don't have to reallocate later + if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) { + allocateBuffer(original); + } + + LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset); + + *reinterpret_cast(spot) = reroute; + // coverity[suspicious_sizeof] + spot = AddrAdd(spot, sizeof(LPVOID)); + bufferList.offset += sizeof(LPVOID); + + JitRuntime runtime; + X86Assembler assembler(&runtime); + addBarrier(reroute, original, assembler); +#if BOOST_ARCH_X86_64 + assembler.mov(rax, imm((intptr_t)(void*)(returnAddress))); + assembler.jmp(rax); +#else + assembler.mov(eax, imm((intptr_t)(void*)(returnAddress))); + assembler.jmp(eax); +#endif + size_t codeSize = assembler.getCodeSize(); + + m_MaxTrampolineSize = + std::max(m_MaxTrampolineSize, static_cast(codeSize + sizeof(LPVOID))); + + // final test to see if we can store the trampoline in the buffer + if ((bufferList.offset + codeSize) > m_BufferSize) { + // can't place function in buffer, allocate another and try again + allocateBuffer(original); + // we could relocate the code and the data but this is simpler + return storeTrampoline(reroute, original, returnAddress); + } + + // adjust relative jumps for move to buffer + codeSize = assembler.relocCode(spot); + + // copy code to buffer + uint8_t* code = assembler.getBuffer(); + memcpy(spot, code, codeSize); + + bufferList.offset += codeSize; + return spot; +} + +#if BOOST_ARCH_X86_64 +void TrampolinePool::copyCode(X86Assembler& assembler, LPVOID source, size_t numBytes) +{ + static UDis86Wrapper disasm; + + disasm.setInputBuffer(static_cast(source), numBytes); + + size_t offset = 0; + + while (ud_disassemble(disasm) != 0) { + // rewrite relative jumps, blind copy everything else + + offset += ud_insn_len(disasm); + + // WARNING: doesn't support conditional jumps + if ((ud_insn_mnemonic(disasm) == UD_Ijmp) && + (ud_insn_opr(disasm, 0)->type == UD_OP_JIMM)) { + uintptr_t dest = disasm.jumpTarget(); + assembler.mov(rax, imm(static_cast(dest))); + assembler.jmp(rax); + } else { + assembler.embed(ud_insn_ptr(&disasm.obj()), ud_insn_len(&disasm.obj())); + // assembler.data(); + } + } +} +#endif + +void TrampolinePool::addCallToStub(X86Assembler& assembler, LPVOID original, + LPVOID reroute) +{ +#if BOOST_ARCH_X86_64 + pushAll(assembler); + assembler.mov(rcx, imm(reinterpret_cast(original))); + assembler.mov(rax, imm((intptr_t)(LPVOID)reroute)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + popAll(assembler); +#else // BOOST_ARCH_X86_64 + assembler.push(reinterpret_cast(original)); + assembler.mov(ecx, imm((intptr_t)(LPVOID)reroute)); + assembler.call(ecx); + assembler.pop(ecx); // remove argument from stack +#endif // BOOST_ARCH_X86_64 +} + +void TrampolinePool::addAbsoluteJump(X86Assembler& assembler, uint64_t destination) +{ +#if BOOST_ARCH_X86_64 + assembler.push(rax); + assembler.push(rax); + assembler.mov(rax, imm(destination)); + assembler.mov(ptr(rsp, 8), rax); + assembler.pop(rax); + assembler.ret(); +#else // BOOST_ARCH_X86_64 + assembler.push(imm(destination)); + assembler.ret(); +#endif // BOOST_ARCH_X86_64 +} + +LPVOID TrampolinePool::storeStub(LPVOID reroute, LPVOID original, size_t preambleSize, + size_t* rerouteOffset) +{ + BufferList& bufferList = getBufferList(original); + // first test to increase likelyhood we don't have to reallocate later + if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) { + allocateBuffer(original); + } + + LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset); + + *reinterpret_cast(spot) = reroute; + // coverity[suspicious_sizeof] + spot = AddrAdd(spot, sizeof(LPVOID)); + bufferList.offset += sizeof(LPVOID); + + JitRuntime runtime; + X86Assembler assembler(&runtime); + addCallToStub(assembler, original, reroute); +#if BOOST_ARCH_X86_64 + // insert backup code + *rerouteOffset = assembler.getCodeSize(); + copyCode(assembler, original, preambleSize); +#else // BOOST_ARCH_X86_64 + assembler.embed(original, preambleSize); +#endif // BOOST_ARCH_X86_64 + addAbsoluteJump(assembler, reinterpret_cast(original) + preambleSize); + + // adjust relative jumps for move to buffer + size_t codeSize = assembler.getCodeSize(); + + m_MaxTrampolineSize = + std::max(m_MaxTrampolineSize, static_cast(codeSize + sizeof(LPVOID))); + + // final test to see if we can store the trampoline in the buffer + if ((bufferList.offset + codeSize) > m_BufferSize) { + // can't place function in buffer, allocate another and try again + allocateBuffer(original); + // we could relocate the code and the data but this is simpler + return storeStub(reroute, original, preambleSize, rerouteOffset); + } + + // copy code to buffer + codeSize = assembler.relocCode(spot); + + bufferList.offset += preambleSize + codeSize; + return spot; +} + +LPVOID TrampolinePool::storeTrampoline(LPVOID reroute, LPVOID original, + size_t preambleSize, size_t* rerouteOffset) +{ + BufferList& bufferList = getBufferList(original); + // first test to increase likelyhood we don't have to reallocate later + if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) { + allocateBuffer(original); + } + + LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset); + + *reinterpret_cast(spot) = reroute; + // coverity[suspicious_sizeof] + spot = AddrAdd(spot, sizeof(LPVOID)); + bufferList.offset += sizeof(LPVOID); + + JitRuntime runtime; + X86Assembler assembler(&runtime); + addBarrier(reroute, original, assembler); + // insert backup code + *rerouteOffset = assembler.getCodeSize(); + assembler.embed(original, static_cast(preambleSize)); + addAbsoluteJump(assembler, reinterpret_cast(original) + preambleSize); + + // adjust relative jumps for move to buffer + size_t codeSize = assembler.getCodeSize(); + + m_MaxTrampolineSize = + std::max(m_MaxTrampolineSize, static_cast(codeSize + sizeof(LPVOID))); + + // TODO this does not take into account that the code size may technically change + // after relocation in which case the following test may determine the code fits into + // the buffer when it really doesnt't. asmjit doesn't seem to provide a way to adjust + // jumps without actually moving the code though + + // final test to see if we can store the trampoline in the buffer + if ((bufferList.offset + codeSize) > m_BufferSize) { + // can't place function in buffer, allocate another and try again + allocateBuffer(original); + // we could relocate the code and the data but this is simpler + return storeTrampoline(reroute, original, preambleSize, rerouteOffset); + } + + // copy code to buffer + codeSize = static_cast(assembler.relocCode(spot)); + + bufferList.offset += preambleSize + codeSize; + + return spot; +} + +LPVOID TrampolinePool::currentBufferAddress(LPVOID addressNear) +{ + LPVOID rounded = roundAddress(addressNear); + auto lookupAddress = m_Buffers.find(rounded); + + if (lookupAddress == m_Buffers.end()) { + lookupAddress = m_Buffers.insert(std::make_pair(rounded, BufferList())).first; + } + if (lookupAddress->second.buffers.size() == 0) { + allocateBuffer(addressNear); + } + + LPVOID res = *(lookupAddress->second.buffers.rbegin()); + return res; +} + +void TrampolinePool::forceUnlockBarrier() +{ + if (m_ThreadGuards.get() != nullptr) { + for (auto funcId : *m_ThreadGuards) { + (*m_ThreadGuards)[funcId.first] = nullptr; + } + } // else no barriers to unlock +} + +TrampolinePool::BufferMap::iterator TrampolinePool::allocateBuffer(LPVOID addressNear) +{ + // allocate a buffer that we can write to and that is executable + SYSTEM_INFO sysInfo; + ::ZeroMemory(&sysInfo, sizeof(SYSTEM_INFO)); + GetSystemInfo(&sysInfo); + + LPVOID rounded = roundAddress(addressNear); + auto iter = m_Buffers.find(rounded); + uintptr_t lowerEnd = reinterpret_cast(rounded); + if (iter->second.buffers.size() > 0) { + // start searching were we last found a buffer + lowerEnd = reinterpret_cast(*iter->second.buffers.rbegin()) + + sysInfo.dwPageSize; + } + + uintptr_t start = + std::max(std::max(lowerEnd, MIN_ALLOC_ADDR), + reinterpret_cast(sysInfo.lpMinimumApplicationAddress)); + uintptr_t upperEnd = reinterpret_cast(rounded) + m_SearchRange; + uintptr_t end = std::min( + upperEnd, reinterpret_cast(sysInfo.lpMaximumApplicationAddress)); + + LPVOID buffer = nullptr; + for (uintptr_t cur = start; cur < end; cur += sysInfo.dwPageSize) { + buffer = VirtualAlloc(reinterpret_cast(cur), m_BufferSize, + MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (buffer != nullptr) { + break; + } + } + if (buffer == nullptr) { + throw std::runtime_error("failed to allocate buffer in range"); + } + + // the caller must have looked up the bufferlist in order to determine that a + // buffer has to be allocated + assert(iter != m_Buffers.end()); + + iter->second.offset = 0; + iter->second.buffers.push_back(buffer); + spdlog::get("usvfs")->debug( + "allocated trampoline buffer for jumps between {0:p} and {1:x} at {2:p}" + "(size {3})", + rounded, (reinterpret_cast(rounded) + m_SearchRange), buffer, + m_BufferSize); + return iter; +} + +LPVOID TrampolinePool::barrier(LPVOID function) +{ + DWORD err = GetLastError(); + LPVOID res = instance().barrierInt(function); + SetLastError(err); + return res; +} + +LPVOID TrampolinePool::release(LPVOID function) +{ + DWORD err = GetLastError(); + LPVOID res = instance().releaseInt(function); + SetLastError(err); + return res; +} + +LPVOID TrampolinePool::barrierInt(LPVOID func) +{ + if (m_FullBlock) { + return nullptr; + } + + if (m_ThreadGuards.get() == nullptr) { + m_ThreadGuards.reset(new TThreadMap()); + } + + auto iter = m_ThreadGuards->find(func); + if ((iter == m_ThreadGuards->end()) || (iter->second == nullptr)) { + (*m_ThreadGuards)[func] = reinterpret_cast(1); + return &(*m_ThreadGuards)[func]; + } else { + return nullptr; + } +} + +LPVOID TrampolinePool::releaseInt(LPVOID func) +{ + DWORD lastError = GetLastError(); + if (m_ThreadGuards.get() == nullptr) { + m_ThreadGuards.reset(new TThreadMap()); + } + + auto iter = m_ThreadGuards->find(func); + if (iter == m_ThreadGuards->end()) { + spdlog::get("hooks")->error("failed to release barrier for func {}", func); + ::SetLastError(lastError); + return nullptr; + } + + LPVOID res = (*m_ThreadGuards)[func]; + (*m_ThreadGuards)[func] = nullptr; + + ::SetLastError(lastError); + return res; +} + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/ttrampolinepool.h b/libs/usvfs/src/thooklib/ttrampolinepool.h new file mode 100644 index 0000000..5b44c51 --- /dev/null +++ b/libs/usvfs/src/thooklib/ttrampolinepool.h @@ -0,0 +1,222 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "asmjit_sane.h" +#include +#include +#include +#include + +#ifdef _MSC_VER +#pragma warning(disable : 4714) +#include +#else +#include +#endif +#include +#include +#include + +// #include + +namespace HookLib +{ + +/// +/// trampolines are runtime-generated mini-functions that are used to call the +/// original code of a function +/// +class TrampolinePool +{ +public: + /// Call initialize before you use the TrampolinePool + static void initialize(); + + static TrampolinePool& instance() + { + // This is a very very sensitive place so we want to keep this function as simple as + // possible and having it inlined probably cann't hurt + return *s_Instance; + } + + void setBlock(bool block); + + /// + /// store a stub without moving code from the original function. This is used in cases + /// where the hook can be placed without overwriting logic (i.e. hot-patchable + /// functions and when chaining hooks) + /// \param reroute the stub function to call before the regular function (on x86 this + /// needs to be cdecl calling convention!) + /// \param original the original function + /// \param returnAddress address under which the original functionality can be + /// reached. + /// for the first hook this should be (original + 2), otherwise + /// the address of the next hook in the chain + /// \return address of the created trampoline function + /// + LPVOID storeStub(LPVOID reroute, LPVOID original, LPVOID returnAddress); + + /// + /// store a stub, moving part of the original function to the trampoline + /// \param reroute the stub function to call before the regular function (on x86 this + /// needs to be cdecl calling convention!) + /// \param original the original function + /// \param preambleSize number of bytes from the original function to backup. Needs to + /// correspond to complete instructions + /// \param rerouteOffset offset in bytes from the created trampoline to the preamble + /// that leads back to the original code + /// \return address of the created trampoline function + /// + LPVOID storeStub(LPVOID reroute, LPVOID original, size_t preambleSize, + size_t* rerouteOffset); + + /// + /// store a trampoline for hot-patchable functions, where the original function + /// is unharmed. + /// \param reroute the reroute function + /// \param original original function + /// \param returnAddress address under which the original functionality can be + /// reached. + /// \return address of the trampoline function + /// + LPVOID storeTrampoline(LPVOID reroute, LPVOID original, LPVOID returnAddress); + + /// + /// store a trampoline, copying a part of the original function to the trampoline. + /// This is used for the case where the hooking mechanism needs to overwrite part of + /// the function + /// \param reroute the reroute function + /// \param original original function + /// \param preambleSize number of bytes from the original function to backup. Needs to + /// correspond to complete instructions + /// \param rerouteOffset offset in bytes from the created trampoline to the preamble + /// that leads us back to the original code + /// \return address of the trampoline function + /// + LPVOID storeTrampoline(LPVOID reroute, LPVOID original, size_t preambleSize, + size_t* rerouteOffset); + + /// + /// \param addressNear used to find a trampoline buffer near the jump instruction + /// \return retrieve address of current trampoline buffer + /// + LPVOID currentBufferAddress(LPVOID addressNear); + + /// + /// \brief forces the barrier(s) for the current thread to be released + /// + void forceUnlockBarrier(); + +private: + struct BufferList + { + size_t offset; + std::vector buffers; + }; + + typedef std::map BufferMap; + static const intptr_t ADDRESS_MASK = + 0xFFFFFFFFFF000000LL; // mask to "round" addresses to consolidate near + // trampolines + +private: + TrampolinePool(); + + TrampolinePool& operator=(const TrampolinePool& reference); // not implemented + + /** + * @brief allocates a buffer with read, write and execute rights near the + * specified adress. The purpose is that we want to be able to jump from + * adressNear to generated code with a 5-byte jump, even on x64 systems. + * @param addressNear the reference adress + * @note the resulting buffer is stored in the m_Buffers map + */ + BufferMap::iterator allocateBuffer(LPVOID addressNear); + + void addBarrier(LPVOID rerouteAddr, LPVOID original, asmjit::X86Assembler& assembler); + +#if BOOST_ARCH_X86_64 + void copyCode(asmjit::X86Assembler& assembler, LPVOID source, size_t numBytes); +#endif // BOOST_ARCH_X86_64 + + BufferList& getBufferList(LPVOID address); + + LPVOID roundAddress(LPVOID address) const; + +public: + static LPVOID __stdcall barrier(LPVOID function); + static LPVOID __stdcall release(LPVOID function); + + LPVOID barrierInt(LPVOID function); + LPVOID releaseInt(LPVOID function); + + void addCallToStub(asmjit::X86Assembler& assembler, LPVOID original, LPVOID reroute); + +private: + /// + /// \brief add a jump to an address outside the custom generated asm code (without + /// modifying registers) + /// \param assembler the assembler generator to write to + /// \param destination destination address + /// \note this currently generates a lot code on x64, may be overly complicated + /// + void addAbsoluteJump(asmjit::X86Assembler& assembler, uint64_t destination); + + DWORD determinePageSize(); + +private: +#if BOOST_ARCH_X86_64 + static const int SIZE_OF_JUMP = 13; +#elif BOOST_ARCH_X86_32 + static const int SIZE_OF_JUMP = 5; +#endif + + // the hook lib tries to allocate buffer close to the hooked functions, + // for 32-bits application, this often falls on 0x40000000, which is the + // address at which DLLs can be loaded on Windows + // + // some old-style DRM (e.g. Dragon Age 2) check that address and prevent + // the game from running if it's not 0x40000000, so we give them a bit of + // spaces before allocating our buffers + // + static constexpr uintptr_t MIN_ALLOC_ADDR = 0x40000000 + 0x200000; + + static TrampolinePool* s_Instance; + + bool m_FullBlock{false}; + + BufferMap m_Buffers; + + typedef std::map TThreadMap; + boost::thread_specific_ptr m_ThreadGuards; + + LPVOID m_BarrierAddr; + LPVOID m_ReleaseAddr; + + DWORD m_BufferSize = {1024}; + size_t m_SearchRange; + uint64_t m_AddressMask; + + int m_MaxTrampolineSize; +}; + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/udis86wrapper.cpp b/libs/usvfs/src/thooklib/udis86wrapper.cpp new file mode 100644 index 0000000..1ac7332 --- /dev/null +++ b/libs/usvfs/src/thooklib/udis86wrapper.cpp @@ -0,0 +1,102 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "udis86wrapper.h" +#include "pch.h" +#include +#include +#include + +namespace HookLib +{ + +UDis86Wrapper::UDis86Wrapper() +{ + ud_init(&m_Obj); + ud_set_syntax(&m_Obj, UD_SYN_INTEL); +#if BOOST_ARCH_X86_64 + ud_set_mode(&m_Obj, 64); +#else + ud_set_mode(&m_Obj, 32); +#endif +} + +void UDis86Wrapper::setInputBuffer(const uint8_t* buffer, size_t size) +{ + m_Buffer = buffer; + ud_set_input_buffer(&m_Obj, buffer, size); + ud_set_pc(&m_Obj, reinterpret_cast(m_Buffer)); +} + +ud_t& UDis86Wrapper::obj() +{ + return m_Obj; +} + +bool UDis86Wrapper::isRelativeJump() +{ + ud_mnemonic_code code = ud_insn_mnemonic(&m_Obj); + // all conditional jumps and loops are relative, as are unconditional jumps with an + // offset operand + return (code == UD_Ijo) || (code == UD_Ijno) || (code == UD_Ijb) || + (code == UD_Ijae) || (code == UD_Ijz) || (code == UD_Ijnz) || + (code == UD_Ijbe) || (code == UD_Ija) || (code == UD_Ijs) || + (code == UD_Ijns) || (code == UD_Ijp) || (code == UD_Ijnp) || + (code == UD_Ijl) || (code == UD_Ijge) || (code == UD_Ijle) || + (code == UD_Ijg) || (code == UD_Ijcxz) || (code == UD_Ijecxz) || + (code == UD_Ijrcxz) || (code == UD_Iloop) || (code == UD_Iloope) || + (code == UD_Iloopne) || + ((code == UD_Icall) && (ud_insn_opr(&m_Obj, 0)->type == UD_OP_JIMM)) || + ((code == UD_Ijmp) && (ud_insn_opr(&m_Obj, 0)->type == UD_OP_JIMM)); +} + +intptr_t UDis86Wrapper::jumpOffset() +{ + const ud_operand_t* op = ud_insn_opr(&m_Obj, 0); + switch (op->size) { + case 8: + return static_cast(op->lval.sbyte); + case 16: + return static_cast(op->lval.sword); + case 32: + return static_cast(op->lval.sdword); + case 64: + return static_cast(op->lval.sqword); + default: + throw std::runtime_error("unsupported jump size"); + } +} + +uint64_t UDis86Wrapper::jumpTarget() +{ + // TODO: assert we're actually on a jump + + uint64_t res = ud_insn_off(&m_Obj) + ud_insn_len(&m_Obj); + + res += jumpOffset(); + + if (ud_insn_opr(&m_Obj, 0)->base == UD_R_RIP) { + res = *reinterpret_cast(res); + } + + return res; +} + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/udis86wrapper.h b/libs/usvfs/src/thooklib/udis86wrapper.h new file mode 100644 index 0000000..48d8a42 --- /dev/null +++ b/libs/usvfs/src/thooklib/udis86wrapper.h @@ -0,0 +1,62 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include +#undef inline // libudis86/types.h defines inline to __inline which is no longer legal + // since vs2012 + +namespace HookLib +{ + +class UDis86Wrapper +{ + +public: + UDis86Wrapper(); + + void setInputBuffer(const uint8_t* buffer, size_t size); + + ud_t& obj(); + + operator ud_t*() { return &m_Obj; } + + bool isRelativeJump(); + + intptr_t jumpOffset(); + + /// + /// determines the absolute jump target at the current instruction, taking into + /// account relative instructions of all sizes and RIP-relative addressing. + /// \return absolute address of the jump at the current disassembler instruction + /// \note this works correctly ONLY if the input buffer has been set with + /// setInputBuffer or + /// if ud_set_pc has been called + /// + uint64_t jumpTarget(); + +private: +private: + ud_t m_Obj; + const uint8_t* m_Buffer{nullptr}; +}; + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/utility.cpp b/libs/usvfs/src/thooklib/utility.cpp new file mode 100644 index 0000000..124141f --- /dev/null +++ b/libs/usvfs/src/thooklib/utility.cpp @@ -0,0 +1,86 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "utility.h" +#include "exceptionex.h" +#include + +namespace HookLib +{ + +FARPROC MyGetProcAddress(HMODULE module, LPCSTR functionName) +{ + // determine position of the exports of the module + PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)module; + if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) { + return nullptr; + } + + PIMAGE_NT_HEADERS ntHeaders = + (PIMAGE_NT_HEADERS)(((LPBYTE)dosHeader) + dosHeader->e_lfanew); + if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) { + return nullptr; + } + + PIMAGE_OPTIONAL_HEADER optionalHeader = &ntHeaders->OptionalHeader; + if (optionalHeader->NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_EXPORT) { + return nullptr; + } + PIMAGE_DATA_DIRECTORY dataDirectory = + &optionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + PIMAGE_EXPORT_DIRECTORY exportDirectory = + (PIMAGE_EXPORT_DIRECTORY)((LPBYTE)dosHeader + dataDirectory->VirtualAddress); + + ULONG* addressOfNames = (ULONG*)((LPBYTE)module + exportDirectory->AddressOfNames); + ULONG* funcAddr = (ULONG*)((LPBYTE)module + exportDirectory->AddressOfFunctions); + + // search exports for the specified name + for (DWORD i = 0; i < exportDirectory->NumberOfNames; ++i) { + char* curFunctionName = (char*)((LPBYTE)module + addressOfNames[i]); + USHORT* nameOrdinals = + (USHORT*)((LPBYTE)module + exportDirectory->AddressOfNameOrdinals); + if (strcmp(functionName, curFunctionName) == 0) { + if (funcAddr[nameOrdinals[i]] >= dataDirectory->VirtualAddress && + funcAddr[nameOrdinals[i]] < + dataDirectory->VirtualAddress + dataDirectory->Size) { + char* forwardLibName = _strdup((LPSTR)module + funcAddr[nameOrdinals[i]]); + ON_BLOCK_EXIT([forwardLibName]() { + free(forwardLibName); + }); + char* forwardFunctionName = strchr(forwardLibName, '.'); + *forwardFunctionName = 0; + ++forwardFunctionName; + + HMODULE forwardLib = LoadLibraryA(forwardLibName); + FARPROC forward = nullptr; + if (forwardLib != nullptr) { + forward = MyGetProcAddress(forwardLib, forwardFunctionName); + FreeLibrary(forwardLib); + } + + return forward; + } + return (FARPROC)((LPBYTE)module + funcAddr[nameOrdinals[i]]); + } + } + return nullptr; +} + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/utility.h b/libs/usvfs/src/thooklib/utility.h new file mode 100644 index 0000000..e0be9f7 --- /dev/null +++ b/libs/usvfs/src/thooklib/utility.h @@ -0,0 +1,35 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include + +namespace HookLib +{ + +/// \brief reimplementation of GetProcAddress to circumvent foreign hooks of +/// GetProcAddress like AcLayer +/// \param module handle to the module that contains the function or variable +/// \param functionName function to retrieve the address of +/// \return address of the exported function +FARPROC MyGetProcAddress(HMODULE module, LPCSTR functionName); + +} // namespace HookLib diff --git a/libs/usvfs/src/tinjectlib/CMakeLists.txt b/libs/usvfs/src/tinjectlib/CMakeLists.txt new file mode 100644 index 0000000..096608e --- /dev/null +++ b/libs/usvfs/src/tinjectlib/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(asmjit CONFIG REQUIRED) + +add_library(tinjectlib STATIC asmjit_sane.h injectlib.cpp injectlib.h) +target_link_libraries(tinjectlib PRIVATE shared asmjit::asmjit) +target_include_directories(tinjectlib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +set_target_properties(tinjectlib PROPERTIES FOLDER injection) diff --git a/libs/usvfs/src/tinjectlib/asmjit_sane.h b/libs/usvfs/src/tinjectlib/asmjit_sane.h new file mode 100644 index 0000000..70ee117 --- /dev/null +++ b/libs/usvfs/src/tinjectlib/asmjit_sane.h @@ -0,0 +1,32 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#pragma warning(push, 3) +#pragma warning(disable : 4201) +#pragma warning(disable : 4244) +#pragma warning(disable : 4245) +// #ifndef ASMJIT_API +// #define ASMJIT_API +// #endif // ASMJIT_API +#include +#include +#pragma warning(pop) diff --git a/libs/usvfs/src/tinjectlib/injectlib.cpp b/libs/usvfs/src/tinjectlib/injectlib.cpp new file mode 100644 index 0000000..2c7362a --- /dev/null +++ b/libs/usvfs/src/tinjectlib/injectlib.cpp @@ -0,0 +1,482 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include +#include + +#include +#include +#include +#include +namespace fs = boost::filesystem; + +#include "injectlib.h" +#include +#include +#include +#include +// local version of asmjit with warning suppression +#include "asmjit_sane.h" +#include +#include + +using namespace asmjit; +using namespace usvfs::shared; + +#if BOOST_ARCH_X86_64 +#pragma message("64bit build") +using namespace x86; +#elif BOOST_ARCH_X86_32 +#pragma message("32bit build") +using namespace asmjit::x86; +#else +#error "unsupported architecture" +#endif + +typedef HMODULE(WINAPI* TLoadLibraryType)(LPCWSTR); +typedef FARPROC(WINAPI* TGetProcAddressType)(HMODULE, LPCSTR); +typedef DWORD(WINAPI* TGetLastErrorType)(); + +typedef BOOL(WINAPI* TSetXStateFeaturesMaskType)(PCONTEXT, DWORD64); + +static const size_t MAX_FUNCTIONAME = 20; + +struct TDataRemote +{ + TLoadLibraryType loadLibrary; + TGetProcAddressType getProcAddress; + TGetLastErrorType getLastError; + REGWORD returnAddress; + + char initFunction[MAX_FUNCTIONAME + 1]; + WCHAR dllName[MAX_PATH]; +}; + +#if BOOST_ARCH_X86_64 +void pushAll(X86Assembler& assembler) +{ + assembler.pushf(); + assembler.push(rax); + assembler.push(rcx); + assembler.push(rdx); + assembler.push(rbx); + assembler.push(rbp); + assembler.push(rsi); + assembler.push(rdi); + assembler.push(r8); + assembler.push(r9); + assembler.push(r10); + assembler.push(r11); + assembler.push(r12); + assembler.push(r13); + assembler.push(r14); + assembler.push(r15); +} + +void popAll(X86Assembler& assembler) +{ + assembler.pop(r15); + assembler.pop(r14); + assembler.pop(r13); + assembler.pop(r12); + assembler.pop(r11); + assembler.pop(r10); + assembler.pop(r9); + assembler.pop(r8); + assembler.pop(rdi); + assembler.pop(rsi); + assembler.pop(rbp); + assembler.pop(rbx); + assembler.pop(rdx); + assembler.pop(rcx); + assembler.pop(rax); + assembler.popf(); +} +#endif // BOOST_ARCH_X86_64 + +void addStub(size_t userDataSize, X86Assembler& assembler, bool skipInit, + TDataRemote* localData, TDataRemote* remoteData, LPCSTR initFunction) +{ + Label Label_DLLLoaded = assembler.newLabel(); + +#if BOOST_ARCH_X86_64 + pushAll(assembler); + // call load library for the actual injection + assembler.mov(rcx, imm(reinterpret_cast(&remoteData->dllName))); + assembler.mov(rax, imm((intptr_t)(void*)localData->loadLibrary)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + + // cancel out of here if we failed to load the dll + // TODO: would be great to report this error. But how? + assembler.test(rax, rax); + assembler.jnz(Label_DLLLoaded); + /* this commented out code may seem pointless but it is a simple way to get at the + error code when debugging. assembler.mov(rax, + imm((intptr_t)(void*)localData->getLastError)); assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + assembler.int3();*/ + popAll(assembler); + assembler.ret(); + assembler.bind(Label_DLLLoaded); + + // determine address of the init function + if (initFunction != nullptr) { + Label Label_SkipInit = assembler.newLabel(); + assembler.mov(rcx, rax); // handle of the dll + assembler.mov(rdx, imm(reinterpret_cast( + remoteData->initFunction))); // name of init function + assembler.mov(rax, imm((intptr_t)(void*)localData->getProcAddress)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + + if (skipInit) { + assembler.test(rax, rax); + assembler.jz(Label_SkipInit); + } + + // call the init function with user data + assembler.mov(rcx, + imm(reinterpret_cast(remoteData) + sizeof(TDataRemote))); + assembler.mov(rdx, imm(static_cast(userDataSize))); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + assembler.bind(Label_SkipInit); + } + + // restore registers + popAll(assembler); +#else + + // save registers + assembler.push(eax); + assembler.pushf(); + + // call load library for the actual injection + assembler.push(imm(void_ptr_cast(remoteData->dllName))); + // assembler.call(ptr_abs(static_cast(remoteData->loadLibrary))); + assembler.mov(eax, imm(void_ptr_cast(localData->loadLibrary))); + assembler.call(eax); + + assembler.test(eax, eax); + assembler.jnz(Label_DLLLoaded); + /* this commented out code may seem pointless but it is a simple way to get at the + error code when debugging. assembler.mov(eax, + imm((intptr_t)(void*)localData->getLastError)); assembler.call(eax); + assembler.int3();*/ + assembler.popf(); + assembler.pop(eax); + assembler.ret(); + assembler.bind(Label_DLLLoaded); + + // determine address of the init function + if (initFunction != nullptr) { + Label Label_SkipInit = assembler.newLabel(); + assembler.push(imm( + void_ptr_cast(remoteData->initFunction))); // name of init function + assembler.push(eax); // handle of the dll + assembler.mov(eax, imm(void_ptr_cast(localData->getProcAddress))); + assembler.call(eax); + if (skipInit) { + assembler.cmp(eax, 0); + assembler.jz(Label_SkipInit); + } else { + assembler.cmp(eax, 0); + assembler.jnz(Label_SkipInit); + // heading for a crash! give an attached debugger a chance to analyse the error + assembler.mov(eax, imm(void_ptr_cast(localData->getLastError))); + assembler.call(eax); + assembler.int3(); + assembler.bind(Label_SkipInit); + } + + // call the init function with user data + assembler.push(userDataSize); + assembler.push(imm(void_ptr_cast(remoteData) + sizeof(TDataRemote))); + assembler.call(eax); + // init function is declared __cdecl so we have to remove parameters from the stack + assembler.pop(eax); + assembler.pop(eax); + if (skipInit) { + assembler.bind(Label_SkipInit); + } + } + + // restore registers + assembler.popf(); + assembler.pop(eax); + +#endif +} + +REGWORD WriteInjectionStub(HANDLE processHandle, LPCWSTR dllName, LPCSTR initFunction, + LPCVOID userData, size_t userDataSize, bool skipInit, + REGWORD returnAddress) +{ + HMODULE k32mod = ::LoadLibrary(__TEXT("kernel32.dll")); + TDataRemote data = {0}; + + if (k32mod != nullptr) { + data.loadLibrary = + reinterpret_cast(GetProcAddress(k32mod, "LoadLibraryW")); + data.getProcAddress = + reinterpret_cast(GetProcAddress(k32mod, "GetProcAddress")); + data.getLastError = + reinterpret_cast(GetProcAddress(k32mod, "GetLastError")); + if ((data.loadLibrary == nullptr) || (data.getProcAddress == nullptr) || + (data.getLastError == nullptr)) { + throw windows_error("failed to determine address for required functions"); + } + } else { + throw windows_error("kernel32.dll not loaded?"); + } + + data.returnAddress = returnAddress; + + if (initFunction != nullptr) { + strncpy_s(data.initFunction, MAX_FUNCTIONAME, initFunction, MAX_FUNCTIONAME); + data.initFunction[MAX_FUNCTIONAME] = '\0'; + } else { + data.initFunction[0] = '\0'; + } + + wcsncpy_s(data.dllName, MAX_PATH, dllName, MAX_PATH - 1); + data.dllName[MAX_PATH - 1] = L'\0'; + + size_t totalSize = sizeof(TDataRemote) + userDataSize; + + // allocate memory in the target process and write the data-block there + LPVOID remoteMem = VirtualAllocEx(processHandle, nullptr, totalSize, + MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + + if (remoteMem == nullptr) { + throw windows_error("failed to allocate memory in target process"); + } + + SIZE_T written; + if (!WriteProcessMemory(processHandle, remoteMem, &data, sizeof(TDataRemote), + &written)) { + throw windows_error("failed to write control data to target process"); + } + if (written != sizeof(TDataRemote)) { + throw windows_error("failed to write whole control data to target process"); + } + + // write user data to remote memory if necessary + if (userData != nullptr) { + if (!WriteProcessMemory(processHandle, AddrAdd(remoteMem, sizeof(TDataRemote)), + userData, userDataSize, &written)) { + throw windows_error("failed to write user data to target process"); + } + if (written != userDataSize) { + throw windows_error("failed to write whole user data to target process"); + } + } + + TDataRemote* remoteData = reinterpret_cast(remoteMem); + + // now for the interesting part: write a stub into the target process that is run + // before any code of the original binary. + + JitRuntime runtime; +#if BOOST_ARCH_X86_64 + X86Assembler assembler(&runtime); + if (returnAddress != 0) { + // put return address on the stack + // (this damages rax which hopefully doesn't matter) + assembler.mov(rax, imm((intptr_t)(void*)data.returnAddress)); + assembler.push(rax); + } // otherwise no return address was specified here. It better be on the stack + // already +#else + X86Assembler assembler(&runtime); + if (returnAddress != 0) { + assembler.push(imm((intptr_t)(void*)data.returnAddress)); + } +#endif + + addStub(userDataSize, assembler, skipInit, &data, remoteData, initFunction); + assembler.ret(0); + + size_t stubSize = assembler.getCodeSize(); + + // reserve memory for the stub + PBYTE stubRemote = reinterpret_cast( + VirtualAllocEx(processHandle, nullptr, stubSize, MEM_COMMIT | MEM_RESERVE, + PAGE_EXECUTE_READWRITE)); + if (stubRemote == nullptr) { + throw windows_error("failed to allocate memory for stub"); + } + + // almost there. copy stub to target process + if (!WriteProcessMemory(processHandle, stubRemote, assembler.getBuffer(), stubSize, + &written) || + (written != stubSize)) { + throw windows_error("failed to write stub to target process"); + } + + return reinterpret_cast(stubRemote); +} + +void InjectDLLEIP(HANDLE processHandle, HANDLE threadHandle, LPCWSTR dllName, + LPCSTR initFunction, LPCVOID userData, size_t userDataSize, + bool skipInit) +{ + threadHandle = + OpenThread((THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME), + FALSE, GetThreadId(threadHandle)); + + if (threadHandle == nullptr) { + throw windows_error("failed to open thread"); + } + + CONTEXT threadContext; + threadContext.ContextFlags = CONTEXT_CONTROL; + + // documentation says starting with Win7 SP1 you HAVE to call SetXStateFeaturesMask + HMODULE k32mod = ::LoadLibrary(__TEXT("kernel32.dll")); + if (k32mod == nullptr) { + throw windows_error("failed to load kernel32.dll"); + } + TSetXStateFeaturesMaskType sxsfm = reinterpret_cast( + GetProcAddress(k32mod, "SetXStateFeaturesMask")); + if (sxsfm != nullptr) { + sxsfm(&threadContext, 0); + } + ::FreeLibrary(k32mod); + + if (GetThreadContext(threadHandle, &threadContext) == 0) { + throw windows_error("failed to access thread context."); + } + +#if BOOST_ARCH_X86_64 + REGWORD returnAddress = threadContext.Rip; +#else + REGWORD returnAddress = threadContext.Eip; +#endif + + REGWORD stubAddress = + WriteInjectionStub(processHandle, dllName, initFunction, userData, userDataSize, + skipInit, returnAddress); + + // make the stub the new next thing for the thread to execute +#if BOOST_ARCH_X86_64 + threadContext.Rip = stubAddress; +#else + threadContext.Eip = stubAddress; +#endif + + if (SetThreadContext(threadHandle, &threadContext) == 0) { + throw windows_error("failed to overwrite thread context"); + } +} + +void InjectDLLRemoteThread(HANDLE processHandle, LPCWSTR dllName, LPCSTR initFunction, + LPCVOID userData, size_t userDataSize, bool skipInit) +{ + REGWORD stubAddress = WriteInjectionStub(processHandle, dllName, initFunction, + userData, userDataSize, skipInit, 0); + + DWORD threadId = 0UL; + HANDLE threadHandle = CreateRemoteThread( + processHandle, nullptr, 0, reinterpret_cast(stubAddress), + nullptr, 0, &threadId); + if (threadHandle == nullptr) { + throw windows_error("failed to start remote thread"); + } + ResumeThread(threadHandle); + spdlog::get("usvfs")->info("waiting for {0:x} to complete", + GetThreadId(threadHandle)); + ::WaitForSingleObject(threadHandle, 100); + ::CloseHandle(threadHandle); +} + +void InjectLib::InjectDLL(HANDLE processHandle, HANDLE threadHandle, LPCWSTR dllName, + LPCSTR initFunction, LPCVOID userData, size_t userDataSize, + bool skipInit) +{ + namespace bfs = boost::filesystem; + if (!exists(bfs::path(dllName))) { + USVFS_THROW_EXCEPTION(file_not_found_error() + << ex_msg(string_cast(dllName))); + } + if (threadHandle == INVALID_HANDLE_VALUE) { +#pragma message( \ + "doesn't seem to work as usvfs causes an exception in the first static initialization or pretty much on any function call. Because process is in different session? CRT related?") + /* + InjectDLLRemoteThread(processHandle, dllName, + initFunction, userData, userDataSize, skipInit); + */ + + DWORD pid = GetProcessId(processHandle); + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + THREADENTRY32 threadInfo; + threadInfo.dwSize = sizeof(THREADENTRY32); + BOOL moreThreads = Thread32First(snapshot, &threadInfo); + std::vector threadHandles; + HANDLE injectThread = INVALID_HANDLE_VALUE; + FILETIME injectThreadTime; + spdlog::get("usvfs")->info("inject dll to process {0}", pid); + while (moreThreads) { + if (threadInfo.th32OwnerProcessID == pid) { + HANDLE thread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, threadInfo.th32ThreadID); + + if (thread != nullptr) { + DWORD suspCount = SuspendThread(thread); + if (suspCount == 0) { + FILETIME creationTime, exitTime, kernelTime, userTime; + ::GetThreadTimes(thread, &creationTime, &exitTime, &kernelTime, &userTime); + + if ((injectThread == INVALID_HANDLE_VALUE) || + (CompareFileTime(&creationTime, &injectThreadTime) < 0)) { + spdlog::get("usvfs")->info("candidate for oldest thread: {0}", + threadInfo.th32ThreadID); + injectThread = thread; + injectThreadTime = creationTime; + } + } + threadHandles.push_back(thread); + } + } + moreThreads = Thread32Next(snapshot, &threadInfo); + } + if (injectThread != INVALID_HANDLE_VALUE) { + spdlog::get("usvfs")->debug("going to inject dll"); + InjectDLLEIP(processHandle, injectThread, dllName, initFunction, userData, + userDataSize, skipInit); + } else { + spdlog::get("usvfs")->critical("found no thread to use for injecting"); + } + + for (HANDLE hdl : threadHandles) { + spdlog::get("usvfs")->info("resuming thread {0}", ::GetThreadId(hdl)); + ResumeThread(hdl); + CloseHandle(hdl); + } + CloseHandle(snapshot); + } else { + InjectDLLEIP(processHandle, threadHandle, dllName, initFunction, userData, + userDataSize, skipInit); + } +} diff --git a/libs/usvfs/src/tinjectlib/injectlib.h b/libs/usvfs/src/tinjectlib/injectlib.h new file mode 100644 index 0000000..f165667 --- /dev/null +++ b/libs/usvfs/src/tinjectlib/injectlib.h @@ -0,0 +1,50 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include + +namespace InjectLib +{ + +/** + * @brief inject a dll into the target process + * @param processHandle handle of the process to inject to + * @param threadHandle handle of the main thread in the process + * @param dllname name/path of the dll to inject. The path can't be longer than MAX_PATH + * characters + * @param initFunction name of the initialization function. Can't be longer than 20 + * characters. If this is null, no function is called. Important: the init function has + * to exist, be exported, take the two userdata parameters and must be __cdecl calling + * convention on 32bit windows! Failing any of these the target process will crash or + * the dll isn't loaded Why __cdecl? Because otherwise we would need .def files to + * export the init function with a GetProcAddress-able function name. + * @param userData data passed to the init function + * @param userDataSize size of the data to be passed + * @param skipInit skip the call to the init function if the named function wasn't found + * in the dll. If false, the target process will crash if the function isn't exported in + * the dll + */ +void InjectDLL(HANDLE processHandle, HANDLE threadHandle, LPCWSTR dllName, + LPCSTR initFunction = nullptr, LPCVOID userData = nullptr, + size_t userDataSize = 0, bool skipInit = false); + +} // namespace InjectLib diff --git a/libs/usvfs/src/usvfs_dll/CMakeLists.txt b/libs/usvfs/src/usvfs_dll/CMakeLists.txt new file mode 100644 index 0000000..2520b50 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required(VERSION 3.16) + +include(GNUInstallDirs) + +find_package(Boost CONFIG REQUIRED COMPONENTS thread) +find_package(spdlog CONFIG REQUIRED) + +file(GLOB sources "*.cpp" "*.h") +source_group("" FILES ${sources}) + +file(GLOB hooks "hooks/*.cpp" "hooks/*.h") +source_group("dlls" FILES ${hooks}) + +add_library(usvfs_dll SHARED) +target_sources(usvfs_dll + PRIVATE + ${PROJECT_SOURCE_DIR}/include/usvfs/usvfsparametersprivate.h + ${sources} + ${hooks} + version.rc + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${PROJECT_SOURCE_DIR}/include + FILES + ${PROJECT_SOURCE_DIR}/include/usvfs/dllimport.h + ${PROJECT_SOURCE_DIR}/include/usvfs/logging.h + ${PROJECT_SOURCE_DIR}/include/usvfs/sharedparameters.h + ${PROJECT_SOURCE_DIR}/include/usvfs/usvfs_version.h + ${PROJECT_SOURCE_DIR}/include/usvfs/usvfs.h + ${PROJECT_SOURCE_DIR}/include/usvfs/usvfsparameters.h +) +target_link_libraries(usvfs_dll + PRIVATE shared thooklib usvfs_helper + Boost::thread spdlog::spdlog_header_only + Shlwapi) +target_compile_definitions(usvfs_dll PRIVATE BUILDING_USVFS_DLL) +set_target_properties(usvfs_dll + PROPERTIES + ARCHIVE_OUTPUT_NAME usvfs${ARCH_POSTFIX} + ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${USVFS_LIBDIR} + ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${USVFS_LIBDIR} + LIBRARY_OUTPUT_NAME usvfs${ARCH_POSTFIX} + LIBRARY_OUTPUT_DIRECTORY_DEBUG ${USVFS_LIBDIR} + LIBRARY_OUTPUT_DIRECTORY_RELEASE ${USVFS_LIBDIR} + PDB_OUTPUT_DIRECTORY_DEBUG ${USVFS_LIBDIR} + PDB_OUTPUT_DIRECTORY_RELEASE ${USVFS_LIBDIR} + RUNTIME_OUTPUT_NAME usvfs${ARCH_POSTFIX} + RUNTIME_OUTPUT_DIRECTORY_DEBUG ${USVFS_LIBDIR} + RUNTIME_OUTPUT_DIRECTORY_RELEASE ${USVFS_LIBDIR}) + +install(TARGETS usvfs_dll EXPORT usvfs${ARCH_POSTFIX}Targets FILE_SET HEADERS) +install(FILES $ DESTINATION pdb OPTIONAL) +install(EXPORT usvfs${ARCH_POSTFIX}Targets + FILE usvfs${ARCH_POSTFIX}Targets.cmake + NAMESPACE usvfs${ARCH_POSTFIX}:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/usvfs +) diff --git a/libs/usvfs/src/usvfs_dll/hookcallcontext.cpp b/libs/usvfs/src/usvfs_dll/hookcallcontext.cpp new file mode 100644 index 0000000..32510c9 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hookcallcontext.cpp @@ -0,0 +1,112 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "hookcallcontext.h" +#include "hookcontext.h" +#include + +namespace usvfs +{ + +class HookStack +{ +public: + static HookStack& instance() + { + PreserveGetLastError boostChangesGetLastError; + if (s_Instance.get() == nullptr) { + s_Instance.reset(new HookStack()); + } + return *s_Instance.get(); + } + + bool setGroup(MutExHookGroup group) + { + if (m_ActiveGroups.test(static_cast(group)) || + m_ActiveGroups.test(static_cast(MutExHookGroup::ALL_GROUPS))) { + return false; + } else { + m_ActiveGroups.set(static_cast(group), true); + return true; + } + } + + void unsetGroup(MutExHookGroup group) + { + m_ActiveGroups.set(static_cast(group), false); + } + +private: + HookStack() {} + +private: + static boost::thread_specific_ptr s_Instance; + std::bitset(MutExHookGroup::LAST)> m_ActiveGroups; +}; + +boost::thread_specific_ptr HookStack::s_Instance; + +HookCallContext::HookCallContext() : m_Active(true), m_Group(MutExHookGroup::NO_GROUP) +{ + updateLastError(); +} + +HookCallContext::HookCallContext(MutExHookGroup group) + : m_Active(HookStack::instance().setGroup(group)), m_Group(group) +{ + updateLastError(); +} + +HookCallContext::~HookCallContext() +{ + if (m_Active && (m_Group != MutExHookGroup::NO_GROUP)) { + HookStack::instance().unsetGroup(m_Group); + } + SetLastError(m_LastError); +} + +void HookCallContext::restoreLastError() +{ + SetLastError(m_LastError); +} + +void HookCallContext::updateLastError(DWORD lastError) +{ + m_LastError = lastError; +} + +bool HookCallContext::active() const +{ + return m_Active; +} + +FunctionGroupLock::FunctionGroupLock(MutExHookGroup group) : m_Group(group) +{ + m_Active = HookStack::instance().setGroup(m_Group); +} + +FunctionGroupLock::~FunctionGroupLock() +{ + if (m_Active) { + HookStack::instance().unsetGroup(m_Group); + } +} + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hookcallcontext.h b/libs/usvfs/src/usvfs_dll/hookcallcontext.h new file mode 100644 index 0000000..ebb3bf9 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hookcallcontext.h @@ -0,0 +1,87 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +namespace usvfs +{ + +/** + * @brief groups of hooks which may be used to implement each other, so only the first + * call should be to one should be manipulated + */ +enum class MutExHookGroup : int +{ + ALL_GROUPS = 0, // An ALL_GROUPS-hook prevents all other hooks from becoming active + // BUT hooks from other groups don't prevent the ALL_GROUPS-hook from + // becoming activated + OPEN_FILE = 1, + CREATE_PROCESS = 2, + FILE_ATTRIBUTES = 3, + FIND_FILES = 4, + LOAD_LIBRARY = 5, + FULL_PATHNAME = 6, + SHELL_FILEOP = 7, + DELETE_FILE = 8, + GET_FILE_VERSION = 9, + GET_MODULE_HANDLE = 10, + SEARCH_FILES = 11, + + NO_GROUP = 12, + LAST = NO_GROUP, +}; + +class HookCallContext +{ + +public: + HookCallContext(); + HookCallContext(MutExHookGroup group); + ~HookCallContext(); + + HookCallContext(const HookCallContext& reference) = delete; + HookCallContext& operator=(const HookCallContext& reference) = delete; + + void restoreLastError(); + + void updateLastError(DWORD lastError = GetLastError()); + + DWORD lastError() const { return m_LastError; } + + bool active() const; + +private: + DWORD m_LastError; + bool m_Active; + MutExHookGroup m_Group; +}; + +class FunctionGroupLock +{ +public: + FunctionGroupLock(MutExHookGroup group); + ~FunctionGroupLock(); + +private: + MutExHookGroup m_Group; + bool m_Active; +}; + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hookcontext.cpp b/libs/usvfs/src/usvfs_dll/hookcontext.cpp new file mode 100644 index 0000000..f47e42d --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hookcontext.cpp @@ -0,0 +1,330 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "hookcontext.h" +#include "exceptionex.h" +#include "hookcallcontext.h" +#include "loghelpers.h" +#include "usvfs.h" +#include +#include +#include +#include + +namespace bi = boost::interprocess; +using usvfs::shared::SharedMemoryT; +using usvfs::shared::VoidAllocatorT; + +using namespace usvfs; +namespace ush = usvfs::shared; + +HookContext* HookContext::s_Instance = nullptr; + +void printBuffer(const char* buffer, size_t size) +{ + static const int bufferSize = 16 * 3; + char temp[bufferSize + 1]; + temp[bufferSize] = '\0'; + + for (size_t i = 0; i < size; ++i) { + size_t offset = i % 16; + _snprintf(&temp[offset * 3], 3, "%02x ", (unsigned char)buffer[i]); + if (offset == 15) { + spdlog::get("hooks")->info("{0:x} - {1}", i - offset, temp); + } + } + + spdlog::get("hooks")->info(temp); +} + +HookContext::HookContext(const usvfsParameters& params, HMODULE module) + : m_ConfigurationSHM(bi::open_or_create, params.instanceName, 64 * 1024), + m_Parameters(retrieveParameters(params)), + m_Tree(m_Parameters->currentSHMName(), + 4 * 1024 * 1024) // 4 MiB empirically covers most small setups without + // need to resize + , + m_InverseTree( + m_Parameters->currentInverseSHMName(), + 128 * 1024) // 128 KiB should cover reverse tree for even larger setups + , + m_DLLModule(module) +{ + if (s_Instance != nullptr) { + throw std::runtime_error("singleton duplicate instantiation (HookContext)"); + } + + const auto userCount = m_Parameters->userConnected(); + + spdlog::get("usvfs")->debug("context current shm: {0} (now {1} connections)", + m_Parameters->currentSHMName(), userCount); + + s_Instance = this; + + if (m_Tree.get() == nullptr) { + USVFS_THROW_EXCEPTION(usage_error() + << ex_msg("shm not found") << ex_msg(params.instanceName)); + } +} + +void HookContext::remove(const char* instanceName) +{ + bi::shared_memory_object::remove(instanceName); +} + +HookContext::~HookContext() +{ + spdlog::get("usvfs")->info("releasing hook context"); + + s_Instance = nullptr; + const auto userCount = m_Parameters->userDisconnected(); + + if (userCount == 0) { + spdlog::get("usvfs")->info("removing tree {}", m_Parameters->instanceName()); + bi::shared_memory_object::remove(m_Parameters->instanceName().c_str()); + } else { + spdlog::get("usvfs")->info("{} users left", userCount); + } +} + +SharedParameters* HookContext::retrieveParameters(const usvfsParameters& params) +{ + std::pair res = + m_ConfigurationSHM.find("parameters"); + + if (res.first == nullptr) { + // not configured yet + spdlog::get("usvfs")->info("create config in {}", ::GetCurrentProcessId()); + + res.first = m_ConfigurationSHM.construct("parameters")( + params, VoidAllocatorT(m_ConfigurationSHM.get_segment_manager())); + + if (res.first == nullptr) { + USVFS_THROW_EXCEPTION(bi::bad_alloc()); + } + } else { + spdlog::get("usvfs")->info("access existing config in {}", ::GetCurrentProcessId()); + } + + spdlog::get("usvfs")->info("{} processes", res.first->registeredProcessCount()); + + return res.first; +} + +HookContext::ConstPtr HookContext::readAccess(const char*) +{ + BOOST_ASSERT(s_Instance != nullptr); + + // TODO: this should be a shared mutex! + s_Instance->m_Mutex.wait(200); + return ConstPtr(s_Instance, unlockShared); +} + +HookContext::Ptr HookContext::writeAccess(const char*) +{ + BOOST_ASSERT(s_Instance != nullptr); + + s_Instance->m_Mutex.wait(200); + return Ptr(s_Instance, unlock); +} + +void HookContext::setDebugParameters(LogLevel level, CrashDumpsType dumpType, + const std::string& dumpPath, + std::chrono::milliseconds delayProcess) +{ + m_Parameters->setDebugParameters(level, dumpType, dumpPath, delayProcess); +} + +void HookContext::updateParameters() const +{ + m_Parameters->setSHMNames(m_Tree.shmName(), m_InverseTree.shmName()); +} + +usvfsParameters HookContext::callParameters() const +{ + updateParameters(); + return m_Parameters->makeLocal(); +} + +std::wstring HookContext::dllPath() const +{ + std::wstring path = winapi::wide::getModuleFileName(m_DLLModule); + return boost::filesystem::path(path).parent_path().make_preferred().wstring(); +} + +void HookContext::registerProcess(DWORD pid) +{ + m_Parameters->registerProcess(pid); +} + +void HookContext::unregisterCurrentProcess() +{ + m_Parameters->unregisterProcess(::GetCurrentProcessId()); +} + +std::vector HookContext::registeredProcesses() const +{ + return m_Parameters->registeredProcesses(); +} + +void HookContext::blacklistExecutable(const std::wstring& wexe) +{ + const auto exe = shared::string_cast(wexe, shared::CodePage::UTF8); + + spdlog::get("usvfs")->debug("blacklisting '{}'", exe); + m_Parameters->blacklistExecutable(exe); +} + +void HookContext::clearExecutableBlacklist() +{ + spdlog::get("usvfs")->debug("clearing blacklist"); + m_Parameters->clearExecutableBlacklist(); +} + +BOOL HookContext::executableBlacklisted(LPCWSTR wapp, LPCWSTR wcmd) const +{ + std::string app; + if (wapp) { + app = ush::string_cast(wapp, ush::CodePage::UTF8); + } + + std::string cmd; + if (wcmd) { + cmd = ush::string_cast(wcmd, ush::CodePage::UTF8); + } + + return m_Parameters->executableBlacklisted(app, cmd); +} + +void usvfs::HookContext::addSkipFileSuffix(const std::wstring& fileSuffix) +{ + const auto fsuffix = + shared::string_cast(fileSuffix, shared::CodePage::UTF8); + + if (fsuffix.empty()) { + return; + } + + spdlog::get("usvfs")->debug("added skip file suffix '{}'", fsuffix); + m_Parameters->addSkipFileSuffix(fsuffix); +} + +void usvfs::HookContext::clearSkipFileSuffixes() +{ + spdlog::get("usvfs")->debug("clearing skip file suffixes"); + m_Parameters->clearSkipFileSuffixes(); +} + +std::vector usvfs::HookContext::skipFileSuffixes() const +{ + return m_Parameters->skipFileSuffixes(); +} + +void usvfs::HookContext::addSkipDirectory(const std::wstring& directory) +{ + const auto dir = shared::string_cast(directory, shared::CodePage::UTF8); + + if (dir.empty()) { + return; + } + + spdlog::get("usvfs")->debug("added skip directory '{}'", dir); + m_Parameters->addSkipDirectory(dir); +} + +void usvfs::HookContext::clearSkipDirectories() +{ + spdlog::get("usvfs")->debug("clearing skip directories"); + m_Parameters->clearSkipDirectories(); +} + +std::vector usvfs::HookContext::skipDirectories() const +{ + return m_Parameters->skipDirectories(); +} + +void HookContext::forceLoadLibrary(const std::wstring& wprocess, + const std::wstring& wpath) +{ + const auto process = + shared::string_cast(wprocess, shared::CodePage::UTF8); + + const auto path = shared::string_cast(wpath, shared::CodePage::UTF8); + + spdlog::get("usvfs")->debug("adding forced library '{}' for process '{}'", path, + process); + + m_Parameters->addForcedLibrary(process, path); +} + +void HookContext::clearLibraryForceLoads() +{ + spdlog::get("usvfs")->debug("clearing forced libraries"); + m_Parameters->clearForcedLibraries(); +} + +std::vector +HookContext::librariesToForceLoad(const std::wstring& processName) +{ + const auto v = m_Parameters->forcedLibraries( + shared::string_cast(processName, shared::CodePage::UTF8)); + + std::vector wv; + for (const auto& s : v) { + wv.push_back(shared::string_cast(s, shared::CodePage::UTF8)); + } + + return wv; +} + +void HookContext::registerDelayed(std::future delayed) +{ + m_Futures.push_back(std::move(delayed)); +} + +std::vector>& HookContext::delayed() +{ + return m_Futures; +} + +void HookContext::unlock(HookContext* instance) +{ + instance->m_Mutex.signal(); +} + +void HookContext::unlockShared(const HookContext* instance) +{ + instance->m_Mutex.signal(); +} + +// deprecated +// +extern "C" DLLEXPORT HookContext* __cdecl CreateHookContext( + const USVFSParameters& oldParams, HMODULE module) +{ + const usvfsParameters p(oldParams); + return usvfsCreateHookContext(p, module); +} + +extern "C" DLLEXPORT usvfs::HookContext* WINAPI +usvfsCreateHookContext(const usvfsParameters& params, HMODULE module) +{ + return new HookContext(params, module); +} diff --git a/libs/usvfs/src/usvfs_dll/hookcontext.h b/libs/usvfs/src/usvfs_dll/hookcontext.h new file mode 100644 index 0000000..c9f6eb1 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hookcontext.h @@ -0,0 +1,224 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "dllimport.h" +#include "redirectiontree.h" +#include "semaphore.h" +#include "tree_container.h" +#include +#include +#include +#include +#include + +namespace usvfs +{ + +class DLLEXPORT SharedParameters; + +/** + * @brief context available to hooks. This is protected by a many-reader + * single-writer mutex + */ +class HookContext +{ + +public: + typedef std::unique_ptr ConstPtr; + typedef std::unique_ptr Ptr; + typedef unsigned int DataIDT; + +public: + HookContext(const usvfsParameters& params, HMODULE module); + + HookContext(const HookContext& reference) = delete; + + DLLEXPORT ~HookContext(); + + HookContext& operator=(const HookContext& reference) = delete; + + static void remove(const char* instance); + + /** + * @brief get read access to the context. + * @return smart ptr to the context. mutex will automatically be released when + * this leaves scope + */ + static ConstPtr readAccess(const char* source); + + /** + * @brief get write access to the context. + * @return smart ptr to the context. mutex will automatically be released when + * this leaves scope + */ + static Ptr writeAccess(const char* source); + + /** + * @return table containing file redirection information + */ + RedirectionTreeContainer& redirectionTable() { return m_Tree; } + + /** + * @return table containing file redirection information + */ + const RedirectionTreeContainer& redirectionTable() const { return m_Tree; } + + RedirectionTreeContainer& inverseTable() { return m_InverseTree; } + + const RedirectionTreeContainer& inverseTable() const { return m_InverseTree; } + + /** + * @return the parameters passed in on dll initialisation + */ + usvfsParameters callParameters() const; + + /** + * @return path to the calling library itself + */ + std::wstring dllPath() const; + + /** + * @brief get access to custom data + * @note the caller gains write access to the data, independent on the lock on + * the context + * as a whole. The caller himself has to ensure thread safety + */ + template + T& customData(DataIDT id) const + { + auto iter = m_CustomData.find(id); + if (iter == m_CustomData.end()) { + iter = m_CustomData.insert(std::make_pair(id, T())).first; + } + // std::map is supposed to not invalidate any iterators when elements are + // added + // so it should be safe to return a pointer here + T* res = boost::any_cast(&iter->second); + return *res; + } + + void registerProcess(DWORD pid); + void unregisterCurrentProcess(); + std::vector registeredProcesses() const; + + void blacklistExecutable(const std::wstring& executableName); + void clearExecutableBlacklist(); + BOOL executableBlacklisted(LPCWSTR lpApplicationName, LPCWSTR lpCommandLine) const; + + void addSkipFileSuffix(const std::wstring& fileSuffix); + void clearSkipFileSuffixes(); + std::vector skipFileSuffixes() const; + + void addSkipDirectory(const std::wstring& directory); + void clearSkipDirectories(); + std::vector skipDirectories() const; + + void forceLoadLibrary(const std::wstring& processName, + const std::wstring& libraryPath); + void clearLibraryForceLoads(); + std::vector librariesToForceLoad(const std::wstring& processName); + + void setDebugParameters(LogLevel level, CrashDumpsType dumpType, + const std::string& dumpPath, + std::chrono::milliseconds delayProcess); + + void updateParameters() const; + + void registerDelayed(std::future delayed); + + std::vector>& delayed(); + +private: + static void unlock(HookContext* instance); + static void unlockShared(const HookContext* instance); + + SharedParameters* retrieveParameters(const usvfsParameters& params); + +private: + static HookContext* s_Instance; + + shared::SharedMemoryT m_ConfigurationSHM; + SharedParameters* m_Parameters{nullptr}; + RedirectionTreeContainer m_Tree; + RedirectionTreeContainer m_InverseTree; + + std::vector> m_Futures; + + mutable std::map m_CustomData; + + HMODULE m_DLLModule; + + // mutable std::recursive_mutex m_Mutex; + mutable RecursiveBenaphore m_Mutex; +}; + +} // namespace usvfs + +extern "C" DLLEXPORT usvfs::HookContext* WINAPI +usvfsCreateHookContext(const usvfsParameters& params, HMODULE module); + +class PreserveGetLastError +{ +public: + PreserveGetLastError() : m_err(GetLastError()) {} + ~PreserveGetLastError() { SetLastError(m_err); } + +private: + DWORD m_err; +}; + +// declare an identifier that is guaranteed to be unique across the application +#define DATA_ID(name) static const usvfs::HookContext::DataIDT name = __COUNTER__ + +// set of macros. These ensure a call context is created but most of all these +// ensure exceptions are caught. + +#define READ_CONTEXT() HookContext::readAccess(__MYFUNC__) +#define WRITE_CONTEXT() HookContext::writeAccess(__MYFUNC__) + +#define HOOK_START_GROUP(group) \ + try { \ + HookCallContext callContext(group); + +#define HOOK_START \ + try { \ + HookCallContext callContext; + +#define HOOK_END \ + } \ + catch (const std::exception& e) \ + { \ + spdlog::get("usvfs")->error("exception in {0}: {1}", __MYFUNC__, e.what()); \ + logExtInfo(e); \ + } + +#define HOOK_ENDP(param) \ + } \ + catch (const std::exception& e) \ + { \ + spdlog::get("usvfs")->error("exception in {0} ({1}): {2}", __MYFUNC__, param, \ + e.what()); \ + logExtInfo(e); \ + } + +#define PRE_REALCALL callContext.restoreLastError(); +#define POST_REALCALL callContext.updateLastError(); diff --git a/libs/usvfs/src/usvfs_dll/hookmanager.cpp b/libs/usvfs/src/usvfs_dll/hookmanager.cpp new file mode 100644 index 0000000..731b856 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hookmanager.cpp @@ -0,0 +1,333 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "hookmanager.h" +#include "../thooklib/ttrampolinepool.h" +#include "../thooklib/utility.h" +#include "exceptionex.h" +#include "hooks/kernel32.h" +#include "hooks/ntdll.h" +#include "usvfs.h" +#include +#include +#include +#include +#include +#include + +using namespace HookLib; +namespace bf = boost::filesystem; + +namespace usvfs +{ + +HookManager* HookManager::s_Instance = nullptr; + +HookManager::HookManager(const usvfsParameters& params, HMODULE module) + : m_Context(params, module) +{ + if (s_Instance != nullptr) { + throw std::runtime_error("singleton duplicate instantiation (HookManager)"); + } + + s_Instance = this; + + m_Context.registerProcess(::GetCurrentProcessId()); + spdlog::get("usvfs")->info("Process registered in shared process list : {}", + ::GetCurrentProcessId()); + + winapi::ex::OSVersion version = winapi::ex::getOSVersion(); + spdlog::get("usvfs")->info( + "Windows version {}.{}.{} sp {} platform {} ({})", version.major, version.minor, + version.build, version.servicpack, version.platformid, + shared::string_cast(winapi::ex::wide::getWindowsBuildLab(true)) + .c_str()); + + initHooks(); + + if (params.debugMode) { + while (!::IsDebuggerPresent()) { + // wait for debugger to attach + ::Sleep(100); + } + } +} + +HookManager::~HookManager() +{ + spdlog::get("hooks")->debug("end hook of process {}", GetCurrentProcessId()); + removeHooks(); + m_Context.unregisterCurrentProcess(); +} + +HookManager& HookManager::instance() +{ + if (s_Instance == nullptr) { + throw std::runtime_error("singleton not instantiated"); + } + + return *s_Instance; +} + +LPVOID HookManager::detour(const char* functionName) +{ + auto iter = m_Hooks.find(functionName); + if (iter != m_Hooks.end()) { + return GetDetour(iter->second); + } else { + return nullptr; + } +} + +void HookManager::removeHook(const std::string& functionName) +{ + auto iter = m_Hooks.find(functionName); + if (iter != m_Hooks.end()) { + try { + RemoveHook(iter->second); + m_Hooks.erase(iter); + spdlog::get("usvfs")->info("removed hook for {}", functionName); + } catch (const std::exception& e) { + spdlog::get("usvfs")->critical("failed to remove hook of {}: {}", functionName, + e.what()); + } + } else { + spdlog::get("usvfs")->info("{} wasn't hooked", functionName); + } +} + +void HookManager::logStubInt(LPVOID address) +{ + if (m_Stubs.find(address) != m_Stubs.end()) { + spdlog::get("hooks")->warn("{0} called", m_Stubs[address]); + } else { + spdlog::get("hooks")->warn("unknown function at {0} called", address); + } +} + +void HookManager::logStub(LPVOID address) +{ + try { + instance().logStubInt(address); + } catch (const std::exception& e) { + spdlog::get("hooks")->debug("function at {0} called after shutdown: {1}", address, + e.what()); + } +} + +void HookManager::installHook(HMODULE module1, HMODULE module2, + const std::string& functionName, LPVOID hook, + LPVOID* fillFuncAddr = nullptr) +{ + BOOST_ASSERT(hook != nullptr); + HOOKHANDLE handle = INVALID_HOOK; + HookError err = ERR_NONE; + LPVOID funcAddr = nullptr; + HMODULE usedModule = nullptr; + // both module1 and module2 are allowed to be null + if (module1 != nullptr) { + funcAddr = MyGetProcAddress(module1, functionName.c_str()); + if (funcAddr != nullptr) { + handle = InstallHook(funcAddr, hook, &err); + } + if (handle != INVALID_HOOK) + usedModule = module1; + } + + if ((handle == INVALID_HOOK) && (module2 != nullptr)) { + funcAddr = MyGetProcAddress(module2, functionName.c_str()); + if (funcAddr != nullptr) { + handle = InstallHook(funcAddr, hook, &err); + } + if (handle != INVALID_HOOK) + usedModule = module2; + } + + if (fillFuncAddr) + *fillFuncAddr = funcAddr; + + if (handle == INVALID_HOOK) { + spdlog::get("usvfs")->error("failed to hook {0}: {1}", functionName, + GetErrorString(err)); + } else { + m_Stubs.insert(make_pair(funcAddr, functionName)); + m_Hooks.insert(make_pair(std::string(functionName), handle)); + spdlog::get("usvfs")->info("hooked {0} ({1}) in {2} type {3}", functionName, + funcAddr, winapi::ansi::getModuleFileName(usedModule), + GetHookType(handle)); + } +} + +void HookManager::installStub(HMODULE module1, HMODULE module2, + const std::string& functionName) +{ + HOOKHANDLE handle = INVALID_HOOK; + HookError err = ERR_NONE; + LPVOID funcAddr = nullptr; + HMODULE usedModule = nullptr; + // both module1 and module2 are allowed to be null + if (module1 != nullptr) { + funcAddr = MyGetProcAddress(module1, functionName.c_str()); + if (funcAddr != nullptr) { + handle = InstallStub(funcAddr, logStub, &err); + } else { + spdlog::get("usvfs")->debug("{} doesn't contain {}", + winapi::ansi::getModuleFileName(module1), + functionName); + } + if (handle != INVALID_HOOK) + usedModule = module1; + } + + if ((handle == INVALID_HOOK) && (module2 != nullptr)) { + funcAddr = MyGetProcAddress(module2, functionName.c_str()); + if (funcAddr != nullptr) { + handle = InstallStub(funcAddr, logStub, &err); + } else { + spdlog::get("usvfs")->debug("{} doesn't contain {}", + winapi::ansi::getModuleFileName(module2), + functionName); + } + if (handle != INVALID_HOOK) + usedModule = module2; + } + + if (handle == INVALID_HOOK) { + spdlog::get("usvfs")->error("failed to stub {0}: {1}", functionName, + GetErrorString(err)); + } else { + m_Stubs.insert(make_pair(funcAddr, functionName)); + m_Hooks.insert(make_pair(std::string(functionName), handle)); + spdlog::get("usvfs")->info("stubbed {0} ({1}) in {2} type {3}", functionName, + funcAddr, winapi::ansi::getModuleFileName(usedModule), + GetHookType(handle)); + } +} + +void HookManager::initHooks() +{ + TrampolinePool::initialize(); + + HookLib::TrampolinePool::instance().setBlock(true); + + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + spdlog::get("usvfs")->debug("kernel32.dll at {0:x}", + reinterpret_cast(k32Mod)); + // kernelbase.dll contains the actual implementation for functions formerly in + // kernel32.dll and advapi32.dll, starting with Windows 7 + // http://msdn.microsoft.com/en-us/library/windows/desktop/dd371752(v=vs.85).aspx + HMODULE kbaseMod = GetModuleHandleA("kernelbase.dll"); + spdlog::get("usvfs")->debug("kernelbase.dll at {0:x}", + reinterpret_cast(kbaseMod)); + + installHook(kbaseMod, k32Mod, "GetFileAttributesExA", hook_GetFileAttributesExA); + installHook(kbaseMod, k32Mod, "GetFileAttributesA", hook_GetFileAttributesA); + installHook(kbaseMod, k32Mod, "GetFileAttributesExW", hook_GetFileAttributesExW); + installHook(kbaseMod, k32Mod, "GetFileAttributesW", hook_GetFileAttributesW); + installHook(kbaseMod, k32Mod, "SetFileAttributesW", hook_SetFileAttributesW); + + installHook(kbaseMod, k32Mod, "CreateDirectoryW", hook_CreateDirectoryW); + installHook(kbaseMod, k32Mod, "RemoveDirectoryW", hook_RemoveDirectoryW); + installHook(kbaseMod, k32Mod, "DeleteFileW", hook_DeleteFileW); + installHook(kbaseMod, k32Mod, "GetCurrentDirectoryA", hook_GetCurrentDirectoryA); + installHook(kbaseMod, k32Mod, "GetCurrentDirectoryW", hook_GetCurrentDirectoryW); + installHook(kbaseMod, k32Mod, "SetCurrentDirectoryA", hook_SetCurrentDirectoryA); + installHook(kbaseMod, k32Mod, "SetCurrentDirectoryW", hook_SetCurrentDirectoryW); + + installHook(kbaseMod, k32Mod, "ExitProcess", hook_ExitProcess); + + installHook(kbaseMod, k32Mod, "CreateProcessInternalW", hook_CreateProcessInternalW, + reinterpret_cast(&CreateProcessInternalW)); + + installHook(kbaseMod, k32Mod, "MoveFileA", hook_MoveFileA); + installHook(kbaseMod, k32Mod, "MoveFileW", hook_MoveFileW); + installHook(kbaseMod, k32Mod, "MoveFileExA", hook_MoveFileExA); + installHook(kbaseMod, k32Mod, "MoveFileExW", hook_MoveFileExW); + installHook(kbaseMod, k32Mod, "MoveFileWithProgressA", hook_MoveFileWithProgressA); + installHook(kbaseMod, k32Mod, "MoveFileWithProgressW", hook_MoveFileWithProgressW); + + installHook(kbaseMod, k32Mod, "CopyFileExW", hook_CopyFileExW); + if (IsWindows8OrGreater()) + installHook(kbaseMod, k32Mod, "CopyFile2", hook_CopyFile2, + reinterpret_cast(&CopyFile2)); + + installHook(kbaseMod, k32Mod, "GetPrivateProfileStringA", + hook_GetPrivateProfileStringA); + installHook(kbaseMod, k32Mod, "GetPrivateProfileStringW", + hook_GetPrivateProfileStringW); + installHook(kbaseMod, k32Mod, "GetPrivateProfileSectionA", + hook_GetPrivateProfileSectionA); + installHook(kbaseMod, k32Mod, "GetPrivateProfileSectionW", + hook_GetPrivateProfileSectionW); + installHook(kbaseMod, k32Mod, "WritePrivateProfileStringA", + hook_WritePrivateProfileStringA); + installHook(kbaseMod, k32Mod, "WritePrivateProfileStringW", + hook_WritePrivateProfileStringW); + + installHook(kbaseMod, k32Mod, "GetFullPathNameA", hook_GetFullPathNameA); + installHook(kbaseMod, k32Mod, "GetFullPathNameW", hook_GetFullPathNameW); + + installHook(kbaseMod, k32Mod, "FindFirstFileExW", hook_FindFirstFileExW); + + HMODULE ntdllMod = GetModuleHandleA("ntdll.dll"); + spdlog::get("usvfs")->debug("ntdll.dll at {0:x}", + reinterpret_cast(ntdllMod)); + installHook(ntdllMod, nullptr, "NtQueryFullAttributesFile", + hook_NtQueryFullAttributesFile); + installHook(ntdllMod, nullptr, "NtQueryAttributesFile", hook_NtQueryAttributesFile); + installHook(ntdllMod, nullptr, "NtQueryDirectoryFile", hook_NtQueryDirectoryFile); + installHook(ntdllMod, nullptr, "NtQueryDirectoryFileEx", hook_NtQueryDirectoryFileEx); + installHook(ntdllMod, nullptr, "NtQueryObject", hook_NtQueryObject); + installHook(ntdllMod, nullptr, "NtQueryInformationFile", hook_NtQueryInformationFile); + installHook(ntdllMod, nullptr, "NtQueryInformationByName", + hook_NtQueryInformationByName); + installHook(ntdllMod, nullptr, "NtOpenFile", hook_NtOpenFile); + installHook(ntdllMod, nullptr, "NtCreateFile", hook_NtCreateFile); + installHook(ntdllMod, nullptr, "NtClose", hook_NtClose); + installHook(ntdllMod, nullptr, "NtTerminateProcess", hook_NtTerminateProcess); + + installHook(kbaseMod, k32Mod, "LoadLibraryExA", hook_LoadLibraryExA); + installHook(kbaseMod, k32Mod, "LoadLibraryExW", hook_LoadLibraryExW); + + // install this hook late as usvfs is calling it itself for debugging purposes + installHook(kbaseMod, k32Mod, "GetModuleFileNameA", hook_GetModuleFileNameA); + installHook(kbaseMod, k32Mod, "GetModuleFileNameW", hook_GetModuleFileNameW); + + spdlog::get("usvfs")->debug("hooks installed"); + HookLib::TrampolinePool::instance().setBlock(false); +} + +void HookManager::removeHooks() +{ + while (m_Hooks.size() > 0) { + auto iter = m_Hooks.begin(); + try { + RemoveHook(iter->second); + spdlog::get("usvfs")->debug("removed hook {}", iter->first); + } catch (const std::exception& e) { + spdlog::get("usvfs")->critical("failed to remove hook: {}", e.what()); + } + + // remove either way, otherwise this is an endless loop + m_Hooks.erase(iter); + } +} + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hookmanager.h b/libs/usvfs/src/usvfs_dll/hookmanager.h new file mode 100644 index 0000000..636a29c --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hookmanager.h @@ -0,0 +1,81 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "hookcontext.h" +#include +#include + +namespace usvfs +{ + +class HookManager +{ +public: + HookManager(const usvfsParameters& params, HMODULE module); + ~HookManager(); + + HookManager(const HookManager& reference) = delete; + + HookManager& operator=(const HookManager& reference) = delete; + + static HookManager& instance(); + + HookContext* context() { return &m_Context; } + + /// + /// \brief retrieve address of the detour of a function + /// \param functionName name of the function to look up + /// \return function address that can be used to directly execute the original code + /// + LPVOID detour(const char* functionName); + + /// + /// \brief remove the hook on the specified function. + /// \param functionName name of the function to unhook + /// \note This function is only exposed to allow a workaround for ExitProcess and may + /// be + /// removed if a better solution is found there. If you have another legit use + /// case, please let me know! + /// + void removeHook(const std::string& functionName); + +private: + void logStubInt(LPVOID address); + static void logStub(LPVOID address); + + void installHook(HMODULE module1, HMODULE module2, const std::string& functionName, + LPVOID hook, LPVOID* fillFuncAddr); + void installStub(HMODULE module1, HMODULE module2, const std::string& functionName); + void initHooks(); + void removeHooks(); + +private: + static HookManager* s_Instance; + + std::map m_Hooks; + + std::map m_Stubs; + + HookContext m_Context; +}; + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h b/libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h new file mode 100644 index 0000000..dbd3597 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h @@ -0,0 +1,263 @@ +#pragma once + +/** + * This header defines function to deal with the various FileInformationClass + * enumeration. + */ + +#include + +#include + +#include + +namespace usvfs::details +{ + +#define DECLARE_HAS_FIELD(Field) \ + template \ + constexpr auto HasFieldImpl##Field(int)->decltype(std::declval().Field, void(), \ + std::true_type()) \ + { \ + return {}; \ + } \ + template \ + constexpr auto HasFieldImpl##Field(...) \ + { \ + return std::false_type{}; \ + } \ + template \ + constexpr auto HasField##Field() \ + { \ + return HasFieldImpl##Field(0); \ + } + +DECLARE_HAS_FIELD(FileName) +DECLARE_HAS_FIELD(NextEntryOffset) +DECLARE_HAS_FIELD(ShortName) + +#undef DECLARE_HAS_FIELD + +template +struct FileInformationClassUtilsImpl +{ + static void get_data(LPCVOID address, ULONG& offset, std::wstring& fileName) + { + const FileInformationClass* info = + reinterpret_cast(address); + + // default value + offset = sizeof(FileInformationClass); + + if constexpr (HasFieldFileName()) { + if constexpr (HasFieldNextEntryOffset()) { + offset = info->NextEntryOffset; + } + fileName = std::wstring(info->FileName, info->FileNameLength / sizeof(WCHAR)); + } + } + + static void set_offset(LPVOID address, ULONG offset) + { + FileInformationClass* info = reinterpret_cast(address); + + if constexpr (HasFieldNextEntryOffset()) { + info->NextEntryOffset = offset; + } + } + + static void set_filename(LPVOID address, const std::wstring& fileName) + { + FileInformationClass* info = reinterpret_cast(address); + + if constexpr (HasFieldFileName()) { + memset(info->FileName, L'\0', info->FileNameLength); + info->FileNameLength = static_cast(fileName.length() * sizeof(WCHAR)); + + // doesn't need to be 0-terminated + memcpy(info->FileName, fileName.c_str(), info->FileNameLength); + + if constexpr (HasFieldShortName()) { + if (info->ShortNameLength > 0) { + info->ShortNameLength = static_cast( + GetShortPathNameW(fileName.c_str(), info->ShortName, 8)); + } + } + } + } +}; + +template +struct FileInformationClassUtils; + +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; +template <> +struct FileInformationClassUtils + : FileInformationClassUtilsImpl +{}; + +// FILE_ALL_INFORMATION needs to be handled differently because it has a field that +// is itself a structure +template <> +struct FileInformationClassUtils +{ + static void get_data(LPCVOID address, ULONG& offset, std::wstring& fileName) + { + FileInformationClassUtils::get_data( + &reinterpret_cast(address)->NameInformation, + offset, fileName); + } + + static void set_offset(LPVOID address, ULONG offset) + { + // this is a no-op but it's consistent to do that everywhere + FileInformationClassUtils::set_offset( + &reinterpret_cast(address)->NameInformation, offset); + } + + static void set_filename(LPVOID address, const std::wstring& fileName) + { + FileInformationClassUtils::set_filename( + &reinterpret_cast(address)->NameInformation, fileName); + } +}; + +} // namespace usvfs::details + +#define _APP_FINFO_CASE(clazz, fn, ...) \ + case clazz: \ + return usvfs::details::FileInformationClassUtils::fn(__VA_ARGS__); + +#define _APPLY_FILEINFO_FN(fn, ...) \ + _APP_FINFO_CASE(FileAllInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileFullDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileBothDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileStandardInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileNameInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileRenameInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileNamesInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileObjectIdInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileReparsePointInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdBothDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdFullDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileNormalizedNameInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdExtdDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdExtdBothDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileId64ExtdDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileId64ExtdBothDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdAllExtdDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdAllExtdBothDirectoryInformation, fn, __VA_ARGS__) + +void GetFileInformationData(FILE_INFORMATION_CLASS fileInformationClass, + LPCVOID address, ULONG& offset, std::wstring& fileName) +{ + switch (fileInformationClass) { + _APPLY_FILEINFO_FN(get_data, address, offset, fileName); + default: + offset = ULONG_MAX; + } +} + +void SetFileInformationOffset(FILE_INFORMATION_CLASS fileInformationClass, + LPVOID address, ULONG offset) +{ + switch (fileInformationClass) { + _APPLY_FILEINFO_FN(set_offset, address, offset); + default: + break; + } +} + +void SetFileInformationFileName(FILE_INFORMATION_CLASS fileInformationClass, + LPVOID address, const std::wstring& fileName) +{ + switch (fileInformationClass) { + _APPLY_FILEINFO_FN(set_filename, address, fileName); + default: + break; + } +} + +#undef _APP_FINFO_CASE +#undef _APPLY_FILEINFO_FN diff --git a/libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp b/libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp new file mode 100644 index 0000000..6599eb0 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp @@ -0,0 +1,1860 @@ +#include "kernel32.h" +#include "sharedids.h" + +#include "../hookcallcontext.h" +#include "../hookcontext.h" +#include "../hookmanager.h" +#include "../maptracker.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ush = usvfs::shared; +using ush::CodePage; +using ush::string_cast; + +namespace usvfs +{ +MapTracker k32DeleteTracker; +MapTracker k32FakeDirTracker; +} // namespace usvfs + +class CurrentDirectoryTracker +{ +public: + using wstring = std::wstring; + + bool get(wstring& currentDir, const wchar_t* forRelativePath = nullptr) + { + int index = m_currentDrive; + if (forRelativePath && *forRelativePath && forRelativePath[1] == ':') + if (!getDriveIndex(forRelativePath, index)) + spdlog::get("usvfs")->warn( + "CurrentDirectoryTracker::get() invalid drive letter: {}, will use current " + "drive {}", + string_cast(forRelativePath), + static_cast('A' + index)); // prints '@' for m_currentDrive == -1 + if (index < 0) + return false; + + std::shared_lock lock(m_mutex); + if (m_perDrive[index].empty()) + return false; + else { + currentDir = m_perDrive[index]; + return true; + } + } + + bool set(const wstring& currentDir) + { + int index = -1; + bool good = !currentDir.empty() && getDriveIndex(currentDir.c_str(), index) && + currentDir[1] == ':'; + std::unique_lock lock(m_mutex); + m_currentDrive = good ? index : -1; + if (good) + m_perDrive[index] = currentDir; + return good; + } + +private: + static bool getDriveIndex(const wchar_t* path, int& index) + { + if (*path >= 'a' && *path <= 'z') + index = *path - 'a'; + else if (*path >= 'A' && *path <= 'Z') + index = *path - 'A'; + else + return false; + return true; + } + + mutable std::shared_mutex m_mutex; + wstring m_perDrive['z' - 'a' + 1]; + int m_currentDrive{-1}; +}; + +CurrentDirectoryTracker k32CurrentDirectoryTracker; + +// attempts to copy source to destination and return the error code +static inline DWORD copyFileDirect(LPCWSTR source, LPCWSTR destination, bool overwrite) +{ + usvfs::FunctionGroupLock lock(usvfs::MutExHookGroup::SHELL_FILEOP); + return CopyFileExW(source, destination, NULL, NULL, NULL, + overwrite ? 0 : COPY_FILE_FAIL_IF_EXISTS) + ? ERROR_SUCCESS + : GetLastError(); +} + +static inline WCHAR pathNameDriveLetter(LPCWSTR path) +{ + if (!path || !path[0]) + return 0; + if (path[1] == ':') + return path[0]; + // if path is not ?: or \* then we need to get absolute path: + std::wstring buf; + if (path[0] != '\\') { + buf = winapi::wide::getFullPathName(path).first; + path = buf.c_str(); + if (!path[0] || path[1] == ':') + return path[0]; + } + // check for \??\C: + if (path[1] && path[2] && path[3] && path[4] && path[0] == '\\' && path[3] == '\\' && + path[5] == ':') + return path[4]; + // give up + return 0; +} + +// returns false also in case we fail to determine the drive letter of the path +static inline bool pathsOnDifferentDrives(LPCWSTR path1, LPCWSTR path2) +{ + WCHAR drive1 = pathNameDriveLetter(path1); + WCHAR drive2 = pathNameDriveLetter(path2); + return drive1 && drive2 && towupper(drive1) != towupper(drive2); +} + +HMODULE WINAPI usvfs::hook_LoadLibraryExA(LPCSTR lpFileName, HANDLE hFile, + DWORD dwFlags) +{ + HMODULE res = nullptr; + + HOOK_START_GROUP(MutExHookGroup::LOAD_LIBRARY) + const std::wstring fileName = ush::string_cast(lpFileName); + + PRE_REALCALL + res = LoadLibraryExW(fileName.c_str(), hFile, dwFlags); + POST_REALCALL + + HOOK_END + return res; +} + +HMODULE WINAPI usvfs::hook_LoadLibraryExW(LPCWSTR lpFileName, HANDLE hFile, + DWORD dwFlags) +{ + HMODULE res = nullptr; + + HOOK_START_GROUP(MutExHookGroup::LOAD_LIBRARY) + // Why is the usual if (!callContext.active()... check missing? + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName); + PRE_REALCALL + res = ::LoadLibraryExW(reroute.fileName(), hFile, dwFlags); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +/// determine name of the binary to run based on parameters for createprocess +std::wstring getBinaryName(LPCWSTR applicationName, LPCWSTR lpCommandLine) +{ + if (applicationName != nullptr) { + std::pair fullPath = + winapi::wide::getFullPathName(applicationName); + return fullPath.second; + } else { + if (lpCommandLine[0] == '"') { + const wchar_t* endQuote = wcschr(lpCommandLine, '"'); + if (endQuote != nullptr) { + return std::wstring(lpCommandLine + 1, endQuote - 1); + } + } + + // according to the documentation, if the commandline is unquoted and has + // spaces, it will be interpreted in multiple ways, i.e. + // c:\program.exe files\sub dir\program name + // c:\program files\sub.exe dir\program name + // c:\program files\sub dir\program.exe name + // c:\program files\sub dir\program name.exe + LPCWSTR space = wcschr(lpCommandLine, L' '); + while (space != nullptr) { + std::wstring subString(lpCommandLine, space); + bool isDirectory = true; + if (winapi::ex::wide::fileExists(subString.c_str(), &isDirectory) && + !isDirectory) { + return subString; + } else { + space = wcschr(space + 1, L' '); + } + } + return std::wstring(lpCommandLine); + } +} + +BOOL(WINAPI* usvfs::CreateProcessInternalW)( + LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken); + +BOOL WINAPI usvfs::hook_CreateProcessInternalW( + LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::CREATE_PROCESS) + if (!callContext.active()) { + res = CreateProcessInternalW( + token, lpApplicationName, lpCommandLine, lpProcessAttributes, + lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, + lpCurrentDirectory, lpStartupInfo, lpProcessInformation, newToken); + callContext.updateLastError(); + return res; + } + + // remember if the caller wanted the process to be suspended. If so, we + // don't resume when we're done + BOOL susp = dwCreationFlags & CREATE_SUSPENDED; + dwCreationFlags |= CREATE_SUSPENDED; + + RerouteW applicationReroute; + RerouteW cmdReroute; + LPWSTR cend = nullptr; + + std::wstring dllPath; + usvfsParameters callParameters; + + { // scope for context lock + auto context = READ_CONTEXT(); + + if (RerouteW::interestingPath(lpCommandLine)) { + // First "argument" in the commandline is the command, we need to identify it and + // reroute it: + if (*lpCommandLine == '"') { + // If the first argument is quoted we trust its is quoted correctly + for (cend = lpCommandLine; *cend && *cend != ' '; ++cend) + if (*cend == '"') { + int escaped = 0; + for (++cend; *cend && (*cend != '"' || escaped % 2 != 0); ++cend) + escaped = *cend == '\\' ? escaped + 1 : 0; + } + + if (*(cend - 1) == '"') + --cend; + auto old_cend = *cend; + *cend = 0; + cmdReroute = RerouteW::create(context, callContext, lpCommandLine + 1); + *cend = old_cend; + if (old_cend == '"') + ++cend; + } else { + // If the first argument we have no choice but to test all the options to quote + // the command as the real CreateProcess will do this: + cend = lpCommandLine; + while (true) { + while (*cend && *cend != ' ') + ++cend; + + auto old_cend = *cend; + *cend = 0; + cmdReroute = RerouteW::create(context, callContext, lpCommandLine); + *cend = old_cend; + if (cmdReroute.wasRerouted() || pathIsFile(cmdReroute.fileName())) + break; + + while (*cend == ' ') + ++cend; + + if (!*cend) { + // if we reached the end of the string we'll just use the whole commandline + // as is: + cend = nullptr; + break; + } + } + } + } + + applicationReroute = RerouteW::create(context, callContext, lpApplicationName); + + dllPath = context->dllPath(); + callParameters = context->callParameters(); + } + + std::wstring cmdline; + if (cend && cmdReroute.fileName()) { + auto fileName = cmdReroute.fileName(); + cmdline.reserve(wcslen(fileName) + wcslen(cend) + 2); + if (*fileName != '"') + cmdline += L"\""; + cmdline += fileName; + if (*fileName != '"') + cmdline += L"\""; + cmdline += cend; + } + + PRE_REALCALL + res = CreateProcessInternalW(token, applicationReroute.fileName(), + cmdline.empty() ? lpCommandLine : &cmdline[0], + lpProcessAttributes, lpThreadAttributes, bInheritHandles, + dwCreationFlags, lpEnvironment, lpCurrentDirectory, + lpStartupInfo, lpProcessInformation, newToken); + POST_REALCALL + + BOOL blacklisted = FALSE; + { // limit scope of context + auto context = READ_CONTEXT(); + blacklisted = context->executableBlacklisted(applicationReroute.fileName(), + cmdReroute.fileName()); + } + + if (res) { + if (!blacklisted) { + try { + injectProcess(dllPath, callParameters, *lpProcessInformation); + } catch (const std::exception& e) { + spdlog::get("hooks")->error("failed to inject into {0}: {1}", + lpApplicationName != nullptr + ? applicationReroute.fileName() + : static_cast(lpCommandLine), + e.what()); + } + } + + // resume unless process is supposed to start suspended + if (!susp && (ResumeThread(lpProcessInformation->hThread) == (DWORD)-1)) { + spdlog::get("hooks")->error("failed to inject into spawned process"); + res = FALSE; + } + } + + LOG_CALL() + .PARAM(lpApplicationName) + .PARAM(applicationReroute.fileName()) + .PARAM(cmdReroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()) + .PARAM(cmdline); + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_GetFileAttributesExA(LPCSTR lpFileName, + GET_FILEEX_INFO_LEVELS fInfoLevelId, + LPVOID lpFileInformation) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = GetFileAttributesExA(lpFileName, fInfoLevelId, lpFileInformation); + callContext.updateLastError(); + return res; + } + HOOK_END + + HOOK_START + const std::wstring fileName = ush::string_cast(lpFileName); + + PRE_REALCALL + res = GetFileAttributesExW(fileName.c_str(), fInfoLevelId, lpFileInformation); + POST_REALCALL + + HOOK_END + return res; +} + +BOOL WINAPI usvfs::hook_GetFileAttributesExW(LPCWSTR lpFileName, + GET_FILEEX_INFO_LEVELS fInfoLevelId, + LPVOID lpFileInformation) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = GetFileAttributesExW(lpFileName, fInfoLevelId, lpFileInformation); + callContext.updateLastError(); + return res; + } + + fs::path canonicalFile = RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)); + + RerouteW reroute = + RerouteW::create(READ_CONTEXT(), callContext, canonicalFile.c_str()); + + PRE_REALCALL + res = ::GetFileAttributesExW(reroute.fileName(), fInfoLevelId, lpFileInformation); + POST_REALCALL + + DWORD originalError = callContext.lastError(); + DWORD fixedError = originalError; + // In case the target does not exist the error value varies to differentiate if the + // parent folder exists (ERROR_FILE_NOT_FOUND) or not (ERROR_PATH_NOT_FOUND). + // If the original target's parent folder doesn't actually exist it may exist in the + // the virtualized sense, or if we rerouted the query the parent of the original path + // might exist while the parent of the rerouted path might not: + if (!res && fixedError == ERROR_PATH_NOT_FOUND) { + // first query original file parent (if we rerouted it): + fs::path originalParent = canonicalFile.parent_path(); + WIN32_FILE_ATTRIBUTE_DATA parentAttr; + if (reroute.wasRerouted() && + ::GetFileAttributesExW(originalParent.c_str(), GetFileExInfoStandard, + &parentAttr) && + (parentAttr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + fixedError = ERROR_FILE_NOT_FOUND; + else { + // now query the rerouted path for parent (which can be different from the parent + // of the rerouted path) + RerouteW rerouteParent = + RerouteW::create(READ_CONTEXT(), callContext, originalParent.c_str()); + if (rerouteParent.wasRerouted() && + ::GetFileAttributesExW(rerouteParent.fileName(), GetFileExInfoStandard, + &parentAttr) && + (parentAttr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + fixedError = ERROR_FILE_NOT_FOUND; + } + } + if (fixedError != originalError) + callContext.updateLastError(fixedError); + + if (reroute.wasRerouted() || fixedError != originalError) { + DWORD resAttrib; + if (res && fInfoLevelId == GetFileExInfoStandard && lpFileInformation) + resAttrib = reinterpret_cast(lpFileInformation) + ->dwFileAttributes; + else + resAttrib = (DWORD)-1; + LOG_CALL() + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(fInfoLevelId) + .PARAMHEX(res) + .PARAMHEX(resAttrib) + .PARAM(originalError) + .PARAM(fixedError); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetFileAttributesA(LPCSTR lpFileName) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = GetFileAttributesA(lpFileName); + callContext.updateLastError(); + return res; + } + HOOK_END + + HOOK_START + const std::wstring fileName = ush::string_cast(lpFileName); + + PRE_REALCALL + res = GetFileAttributesW(fileName.c_str()); + POST_REALCALL + + HOOK_END + return res; +} + +DWORD WINAPI usvfs::hook_GetFileAttributesW(LPCWSTR lpFileName) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = GetFileAttributesW(lpFileName); + callContext.updateLastError(); + return res; + } + + fs::path canonicalFile = RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)); + + RerouteW reroute = + RerouteW::create(READ_CONTEXT(), callContext, canonicalFile.c_str()); + + if (reroute.wasRerouted()) + PRE_REALCALL + res = ::GetFileAttributesW(reroute.fileName()); + POST_REALCALL + + DWORD originalError = callContext.lastError(); + DWORD fixedError = originalError; + // In case the target does not exist the error value varies to differentiate if the + // parent folder exists (ERROR_FILE_NOT_FOUND) or not (ERROR_PATH_NOT_FOUND). + // If the original target's parent folder doesn't actually exist it may exist in the + // the virtualized sense, or if we rerouted the query the parent of the original path + // might exist while the parent of the rerouted path might not: + if (res == INVALID_FILE_ATTRIBUTES && fixedError == ERROR_PATH_NOT_FOUND) { + // first query original file parent (if we rerouted it): + fs::path originalParent = canonicalFile.parent_path(); + DWORD attr; + if (reroute.wasRerouted() && + (attr = ::GetFileAttributesW(originalParent.c_str())) != + INVALID_FILE_ATTRIBUTES && + (attr & FILE_ATTRIBUTE_DIRECTORY)) + fixedError = ERROR_FILE_NOT_FOUND; + else { + // now query the rerouted path for parent (which can be different from the parent + // of the rerouted path) + RerouteW rerouteParent = + RerouteW::create(READ_CONTEXT(), callContext, originalParent.c_str()); + if (rerouteParent.wasRerouted() && + (attr = ::GetFileAttributesW(rerouteParent.fileName())) != + INVALID_FILE_ATTRIBUTES && + (attr & FILE_ATTRIBUTE_DIRECTORY)) + fixedError = ERROR_FILE_NOT_FOUND; + } + } + if (fixedError != originalError) + callContext.updateLastError(fixedError); + + if (reroute.wasRerouted() || fixedError != originalError) { + LOG_CALL() + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(originalError) + .PARAM(fixedError); + } + + HOOK_ENDP(lpFileName); + + return res; +} + +DWORD WINAPI usvfs::hook_SetFileAttributesW(LPCWSTR lpFileName, DWORD dwFileAttributes) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + // Why is the usual if (!callContext.active()... check missing? + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName); + PRE_REALCALL + res = ::SetFileAttributesW(reroute.fileName(), dwFileAttributes); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL().PARAM(reroute.fileName()).PARAM(res).PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_DeleteFileW(LPCWSTR lpFileName) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::DELETE_FILE) + // Why is the usual if (!callContext.active()... check missing? + + const std::wstring path = + RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)).wstring(); + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, path.c_str()); + + PRE_REALCALL + if (reroute.wasRerouted()) { + res = ::DeleteFileW(reroute.fileName()); + } else { + res = ::DeleteFileW(path.c_str()); + } + POST_REALCALL + + if (res) { + reroute.removeMapping(READ_CONTEXT()); + } + + if (reroute.wasRerouted()) + LOG_CALL() + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +BOOL rewriteChangedDrives(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, + const usvfs::RerouteW& readReroute, + const usvfs::CreateRerouter& writeReroute) +{ + return ((readReroute.wasRerouted() || writeReroute.wasRerouted()) && + pathsOnDifferentDrives(readReroute.fileName(), writeReroute.fileName()) && + !pathsOnDifferentDrives(lpExistingFileName, lpNewFileName)); +} + +BOOL WINAPI usvfs::hook_MoveFileA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + + if (!callContext.active()) { + res = MoveFileA(lpExistingFileName, lpNewFileName); + callContext.updateLastError(); + return res; + } + + HOOK_END + HOOK_START + + const auto& existingFileName = ush::string_cast(lpExistingFileName); + const auto& newFileName = ush::string_cast(lpNewFileName); + + PRE_REALCALL + res = MoveFileW(existingFileName.c_str(), newFileName.c_str()); + POST_REALCALL + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = MoveFileW(lpExistingFileName, lpNewFileName); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + CreateRerouter writeReroute; + bool callOriginal = true; + DWORD newFlags = 0; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, lpExistingFileName); + callOriginal = writeReroute.rerouteNew(context, callContext, lpNewFileName, false, + "hook_MoveFileW"); + } + + if (callOriginal) { + bool movedDrives = rewriteChangedDrives(lpExistingFileName, lpNewFileName, + readReroute, writeReroute); + if (movedDrives) + newFlags |= MOVEFILE_COPY_ALLOWED; + + bool isDirectory = pathIsDirectory(readReroute.fileName()); + + PRE_REALCALL + if (isDirectory && movedDrives) { + SHFILEOPSTRUCTW sf = {0}; + sf.wFunc = FO_MOVE; + sf.hwnd = 0; + sf.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI; + sf.pFrom = readReroute.fileName(); + sf.pTo = writeReroute.fileName(); + int shRes = ::SHFileOperationW(&sf); + switch (shRes) { + case 0x78: + callContext.updateLastError(ERROR_ACCESS_DENIED); + break; + case 0x7C: + callContext.updateLastError(ERROR_FILE_NOT_FOUND); + break; + case 0x7E: + case 0x80: + callContext.updateLastError(ERROR_FILE_EXISTS); + break; + default: + callContext.updateLastError(shRes); + } + res = shRes == 0; + } else if (newFlags) + res = ::MoveFileExW(readReroute.fileName(), writeReroute.fileName(), newFlags); + else + res = ::MoveFileW(readReroute.fileName(), writeReroute.fileName()); + POST_REALCALL + + if (res) + SetLastError(ERROR_SUCCESS); + + writeReroute.updateResult(callContext, res); + + if (res) { + readReroute.removeMapping( + READ_CONTEXT(), isDirectory); // Updating the rerouteCreate to check deleted + // file entries should make this okay + + if (writeReroute.newReroute()) { + if (isDirectory) + RerouteW::addDirectoryMapping(WRITE_CONTEXT(), fs::path(lpNewFileName), + fs::path(writeReroute.fileName())); + else + writeReroute.insertMapping(WRITE_CONTEXT()); + } + } + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAMWRAP(newFlags) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileExA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName, + DWORD dwFlags) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + + if (!callContext.active()) { + res = MoveFileExA(lpExistingFileName, lpNewFileName, dwFlags); + callContext.updateLastError(); + return res; + } + + HOOK_END + HOOK_START + + const std::wstring existingFileName = + ush::string_cast(lpExistingFileName); + + // careful: lpNewFileName can be null if dwFlags is + // MOVEFILE_DELAY_UNTIL_REBOOT, so don't blindly cast the string and make sure + // the null pointer is forwarded correctly + std::wstring newFileNameWstring; + const wchar_t* newFileName = nullptr; + + if (lpNewFileName) { + newFileNameWstring = ush::string_cast(lpNewFileName); + newFileName = newFileNameWstring.c_str(); + } + + PRE_REALCALL + res = MoveFileExW(existingFileName.c_str(), newFileName, dwFlags); + POST_REALCALL + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, + DWORD dwFlags) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = MoveFileExW(lpExistingFileName, lpNewFileName, dwFlags); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + CreateRerouter writeReroute; + bool callOriginal = true; + DWORD newFlags = dwFlags; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, lpExistingFileName); + callOriginal = writeReroute.rerouteNew(context, callContext, lpNewFileName, + newFlags & MOVEFILE_REPLACE_EXISTING, + "hook_MoveFileExW"); + } + + if (callOriginal) { + bool movedDrives = rewriteChangedDrives(lpExistingFileName, lpNewFileName, + readReroute, writeReroute); + + bool isDirectory = pathIsDirectory(readReroute.fileName()); + + PRE_REALCALL + if (isDirectory && movedDrives) { + SHFILEOPSTRUCTW sf = {0}; + sf.wFunc = FO_MOVE; + sf.hwnd = 0; + sf.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI; + sf.pFrom = readReroute.fileName(); + sf.pTo = writeReroute.fileName(); + int shRes = ::SHFileOperationW(&sf); + switch (shRes) { + case 0x78: + callContext.updateLastError(ERROR_ACCESS_DENIED); + break; + case 0x7C: + callContext.updateLastError(ERROR_FILE_NOT_FOUND); + break; + case 0x7E: + case 0x80: + callContext.updateLastError(ERROR_FILE_EXISTS); + break; + default: + callContext.updateLastError(shRes); + } + res = shRes == 0; + } else + res = ::MoveFileExW(readReroute.fileName(), writeReroute.fileName(), newFlags); + POST_REALCALL + + if (res) + SetLastError(ERROR_SUCCESS); + + writeReroute.updateResult(callContext, res); + + if (res) { + readReroute.removeMapping( + READ_CONTEXT(), isDirectory); // Updating the rerouteCreate to check deleted + // file entries should make this okay + + if (writeReroute.newReroute()) { + if (isDirectory) + RerouteW::addDirectoryMapping(WRITE_CONTEXT(), fs::path(lpNewFileName), + fs::path(writeReroute.fileName())); + else + writeReroute.insertMapping(WRITE_CONTEXT()); + } + } + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAMWRAP(dwFlags) + .PARAMWRAP(newFlags) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileWithProgressA(LPCSTR lpExistingFileName, + LPCSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, DWORD dwFlags) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + + if (!callContext.active()) { + res = MoveFileWithProgressA(lpExistingFileName, lpNewFileName, lpProgressRoutine, + lpData, dwFlags); + callContext.updateLastError(); + return res; + } + + HOOK_END + HOOK_START + + const auto& existingFileName = ush::string_cast(lpExistingFileName); + + // careful: lpNewFileName can be null if dwFlags is + // MOVEFILE_DELAY_UNTIL_REBOOT, so don't blindly cast the string and make sure + // the null pointer is forwarded correctly + std::wstring newFileNameWstring; + const wchar_t* newFileName = nullptr; + + if (lpNewFileName) { + newFileNameWstring = ush::string_cast(lpNewFileName); + newFileName = newFileNameWstring.c_str(); + } + + PRE_REALCALL + res = MoveFileWithProgressW(existingFileName.c_str(), newFileName, lpProgressRoutine, + lpData, dwFlags); + POST_REALCALL + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileWithProgressW(LPCWSTR lpExistingFileName, + LPCWSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, DWORD dwFlags) +{ + + // TODO: Remove all redundant hooks to moveFile alternatives. + // it would appear that all other moveFile functions end up calling this one with no + // additional code. + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = MoveFileWithProgressW(lpExistingFileName, lpNewFileName, lpProgressRoutine, + lpData, dwFlags); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + usvfs::CreateRerouter writeReroute; + bool callOriginal = true; + DWORD newFlags = dwFlags; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, lpExistingFileName); + callOriginal = writeReroute.rerouteNew(context, callContext, lpNewFileName, + newFlags & MOVEFILE_REPLACE_EXISTING, + "hook_MoveFileWithProgressW"); + } + + if (callOriginal) { + bool movedDrives = rewriteChangedDrives(lpExistingFileName, lpNewFileName, + readReroute, writeReroute); + if (movedDrives) + newFlags |= MOVEFILE_COPY_ALLOWED; + + bool isDirectory = pathIsDirectory(readReroute.fileName()); + + PRE_REALCALL + if (isDirectory && movedDrives) { + SHFILEOPSTRUCTW sf = {0}; + sf.wFunc = FO_MOVE; + sf.hwnd = 0; + sf.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI; + sf.pFrom = readReroute.fileName(); + sf.pTo = writeReroute.fileName(); + int shRes = ::SHFileOperationW(&sf); + switch (shRes) { + case 0x78: + callContext.updateLastError(ERROR_ACCESS_DENIED); + break; + case 0x7C: + callContext.updateLastError(ERROR_FILE_NOT_FOUND); + break; + case 0x7E: + case 0x80: + callContext.updateLastError(ERROR_FILE_EXISTS); + break; + default: + callContext.updateLastError(shRes); + } + res = shRes == 0; + } else + res = ::MoveFileWithProgressW(readReroute.fileName(), writeReroute.fileName(), + lpProgressRoutine, lpData, newFlags); + POST_REALCALL + + if (res) + SetLastError(ERROR_SUCCESS); + + writeReroute.updateResult(callContext, res); + + if (res) { + // TODO: this call causes the node to be removed twice in case of + // MOVEFILE_COPY_ALLOWED as the deleteFile hook lower level already takes care of + // it, but deleteFile can't be disabled since we are relying on it in case of + // MOVEFILE_REPLACE_EXISTING for the destination file. + readReroute.removeMapping( + READ_CONTEXT(), + isDirectory); // Updating the rerouteCreate to check deleted file entries + // should make this okay (not related to comments above) + + if (writeReroute.newReroute()) { + if (isDirectory) + RerouteW::addDirectoryMapping(WRITE_CONTEXT(), fs::path(lpNewFileName), + fs::path(writeReroute.fileName())); + else + writeReroute.insertMapping(WRITE_CONTEXT()); + } + } + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAMWRAP(dwFlags) + .PARAMWRAP(newFlags) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_CopyFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, LPVOID lpData, + LPBOOL pbCancel, DWORD dwCopyFlags) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = CopyFileExW(lpExistingFileName, lpNewFileName, lpProgressRoutine, lpData, + pbCancel, dwCopyFlags); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + usvfs::CreateRerouter writeReroute; + bool callOriginal = true; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, lpExistingFileName); + callOriginal = writeReroute.rerouteNew( + context, callContext, lpNewFileName, + (dwCopyFlags & COPY_FILE_FAIL_IF_EXISTS) == 0, "hook_CopyFileExW"); + } + + if (callOriginal) { + PRE_REALCALL + res = ::CopyFileExW(readReroute.fileName(), writeReroute.fileName(), + lpProgressRoutine, lpData, pbCancel, dwCopyFlags); + POST_REALCALL + writeReroute.updateResult(callContext, res); + + if (res && writeReroute.newReroute()) + writeReroute.insertMapping(WRITE_CONTEXT()); + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer) +{ + DWORD res = 0; + + HOOK_START + + std::wstring buffer; + buffer.resize(nBufferLength); + + PRE_REALCALL + res = GetCurrentDirectoryW(nBufferLength, &buffer[0]); + POST_REALCALL + + if (res > 0) { + res = WideCharToMultiByte(CP_ACP, 0, buffer.c_str(), res + 1, lpBuffer, + nBufferLength, nullptr, nullptr); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer) +{ + DWORD res = FALSE; + + HOOK_START + + std::wstring actualCWD; + + if (!k32CurrentDirectoryTracker.get(actualCWD)) { + PRE_REALCALL + res = ::GetCurrentDirectoryW(nBufferLength, lpBuffer); + POST_REALCALL + } else { + ush::wcsncpy_sz(lpBuffer, &actualCWD[0], + std::min(static_cast(nBufferLength), actualCWD.size() + 1)); + + // yupp, that's how GetCurrentDirectory actually works... + if (actualCWD.size() < nBufferLength) { + res = static_cast(actualCWD.size()); + } else { + res = static_cast(actualCWD.size() + 1); + } + } + + if (nBufferLength) + LOG_CALL() + .PARAM(std::wstring(lpBuffer, res)) + .PARAM(nBufferLength) + .PARAM(actualCWD.size()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_SetCurrentDirectoryA(LPCSTR lpPathName) +{ + return SetCurrentDirectoryW(ush::string_cast(lpPathName).c_str()); +} + +BOOL WINAPI usvfs::hook_SetCurrentDirectoryW(LPCWSTR lpPathName) +{ + BOOL res = FALSE; + + HOOK_START + + const fs::path& realPath = RerouteW::canonizePath(RerouteW::absolutePath(lpPathName)); + const std::wstring& realPathStr = realPath.wstring(); + std::wstring finalRoute; + BOOL found = FALSE; + + if (fs::exists(realPath)) + finalRoute = realPathStr; + else { + WCHAR processDir[MAX_PATH]; + if (::GetModuleFileNameW(NULL, processDir, MAX_PATH) != 0 && + ::PathRemoveFileSpecW(processDir)) { + WCHAR processName[MAX_PATH]; + ::GetModuleFileNameW(NULL, processName, MAX_PATH); + fs::path routedName = realPath / processName; + RerouteW rerouteTest = + RerouteW::create(READ_CONTEXT(), callContext, routedName.wstring().c_str()); + if (rerouteTest.wasRerouted()) { + std::wstring reroutedPath = rerouteTest.fileName(); + if (routedName.wstring().find(processDir) != std::string::npos) { + fs::path finalPath(reroutedPath); + finalRoute = finalPath.parent_path().wstring(); + found = TRUE; + } + } + } + + if (!found) { + RerouteW reroute = + RerouteW::create(READ_CONTEXT(), callContext, realPathStr.c_str()); + finalRoute = reroute.fileName(); + } + } + + PRE_REALCALL + res = ::SetCurrentDirectoryW(finalRoute.c_str()); + POST_REALCALL + + if (res) + if (!k32CurrentDirectoryTracker.set(realPathStr)) + spdlog::get("usvfs")->warn("Updating actual current directory failed: {} ?!", + string_cast(realPathStr)); + + LOG_CALL() + .PARAM(lpPathName) + .PARAM(realPathStr) + .PARAM(finalRoute) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DLLEXPORT BOOL WINAPI usvfs::hook_CreateDirectoryW( + LPCWSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes) +{ + BOOL res = FALSE; + HOOK_START + + RerouteW reroute = RerouteW::createOrNew(READ_CONTEXT(), callContext, lpPathName); + + PRE_REALCALL + res = ::CreateDirectoryW(reroute.fileName(), lpSecurityAttributes); + POST_REALCALL + + if (res && reroute.newReroute()) + reroute.insertMapping(WRITE_CONTEXT(), true); + + if (reroute.wasRerouted()) + LOG_CALL() + .PARAM(lpPathName) + .PARAM(reroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DLLEXPORT BOOL WINAPI usvfs::hook_RemoveDirectoryW(LPCWSTR lpPathName) +{ + + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::DELETE_FILE) + // Why is the usual if (!callContext.active()... check missing? + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpPathName); + + PRE_REALCALL + if (reroute.wasRerouted()) { + res = ::RemoveDirectoryW(reroute.fileName()); + } else { + res = ::RemoveDirectoryW(lpPathName); + } + POST_REALCALL + + if (res) { + reroute.removeMapping(READ_CONTEXT(), true); + } + + if (reroute.wasRerouted()) + LOG_CALL() + .PARAM(lpPathName) + .PARAM(reroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetFullPathNameA(LPCSTR lpFileName, DWORD nBufferLength, + LPSTR lpBuffer, LPSTR* lpFilePart) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::FULL_PATHNAME) + if (!callContext.active()) { + res = GetFullPathNameA(lpFileName, nBufferLength, lpBuffer, lpFilePart); + callContext.updateLastError(); + return res; + } + + std::string resolvedWithCMD; + + std::wstring actualCWD; + fs::path filePath = ush::string_cast(lpFileName, CodePage::UTF8); + if (k32CurrentDirectoryTracker.get(actualCWD, filePath.wstring().c_str())) { + if (!filePath.is_absolute()) + resolvedWithCMD = ush::string_cast( + (actualCWD / filePath.relative_path()).wstring()); + } + + PRE_REALCALL + res = + ::GetFullPathNameA(resolvedWithCMD.empty() ? lpFileName : resolvedWithCMD.c_str(), + nBufferLength, lpBuffer, lpFilePart); + POST_REALCALL + + if (false && nBufferLength) + LOG_CALL() + .PARAM(lpFileName) + .PARAM(resolvedWithCMD) + .PARAM(std::string(lpBuffer, res)) + .PARAM(nBufferLength) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetFullPathNameW(LPCWSTR lpFileName, DWORD nBufferLength, + LPWSTR lpBuffer, LPWSTR* lpFilePart) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::FULL_PATHNAME) + if (!callContext.active()) { + res = GetFullPathNameW(lpFileName, nBufferLength, lpBuffer, lpFilePart); + callContext.updateLastError(); + return res; + } + + std::wstring resolvedWithCMD; + + std::wstring actualCWD; + if (k32CurrentDirectoryTracker.get(actualCWD, lpFileName)) { + fs::path filePath = lpFileName; + if (!filePath.is_absolute()) + resolvedWithCMD = (actualCWD / filePath.relative_path()).wstring(); + } + + PRE_REALCALL + res = + ::GetFullPathNameW(resolvedWithCMD.empty() ? lpFileName : resolvedWithCMD.c_str(), + nBufferLength, lpBuffer, lpFilePart); + POST_REALCALL + + if (false && nBufferLength) + LOG_CALL() + .PARAM(lpFileName) + .PARAM(resolvedWithCMD) + .PARAM(std::wstring(lpBuffer, res)) + .PARAM(nBufferLength) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetModuleFileNameA(HMODULE hModule, LPSTR lpFilename, + DWORD nSize) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS) + + PRE_REALCALL + res = ::GetModuleFileNameA(hModule, lpFilename, nSize); + POST_REALCALL + if ((res != 0) && callContext.active()) { + std::vector buf; + // If GetModuleFileNameA failed because the buffer is not large enough this + // complicates matters because we are dealing with incomplete information + // (consider for example the case that we have a long real path which will + // be routed to a short virtual so the call should actually succeed in such + // a case). To solve this we simply use our own buffer to find the complete + // module path: + DWORD full_res = res; + size_t buf_size = nSize; + while (full_res == buf_size) { + buf_size = std::max(static_cast(MAX_PATH), buf_size * 2); + buf.resize(buf_size); + full_res = ::GetModuleFileNameA(hModule, buf.data(), buf_size); + } + + RerouteW reroute = RerouteW::create( + READ_CONTEXT(), callContext, + ush::string_cast(buf.empty() ? lpFilename : buf.data()).c_str(), + true); + if (reroute.wasRerouted()) { + DWORD reroutedSize = static_cast(wcslen(reroute.fileName())); + if (reroutedSize >= nSize) { + reroutedSize = nSize - 1; + callContext.updateLastError(ERROR_INSUFFICIENT_BUFFER); + res = nSize; + } else + res = reroutedSize; + memcpy(lpFilename, ush::string_cast(reroute.fileName()).c_str(), + reroutedSize * sizeof(lpFilename[0])); + lpFilename[reroutedSize] = 0; + + LOG_CALL() + .PARAM(hModule) + .addParam("lpFilename", (res != 0UL) ? lpFilename : "") + .PARAM(nSize) + .PARAM(res) + .PARAM(callContext.lastError()); + } + } + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetModuleFileNameW(HMODULE hModule, LPWSTR lpFilename, + DWORD nSize) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS) + + PRE_REALCALL + res = ::GetModuleFileNameW(hModule, lpFilename, nSize); + POST_REALCALL + if ((res != 0) && callContext.active()) { + std::vector buf; + // If GetModuleFileNameW failed because the buffer is not large enough this + // complicates matters because we are dealing with incomplete information (consider + // for example the case that we have a long real path which will be routed to a + // short virtual so the call should actually succeed in such a case). To solve this + // we simply use our own buffer to find the complete module path: + DWORD full_res = res; + size_t buf_size = nSize; + while (full_res == buf_size) { + buf_size = std::max(static_cast(MAX_PATH), buf_size * 2); + buf.resize(buf_size); + full_res = ::GetModuleFileNameW(hModule, buf.data(), buf_size); + } + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, + buf.empty() ? lpFilename : buf.data(), true); + if (reroute.wasRerouted()) { + DWORD reroutedSize = static_cast(wcslen(reroute.fileName())); + if (reroutedSize >= nSize) { + reroutedSize = nSize - 1; + callContext.updateLastError(ERROR_INSUFFICIENT_BUFFER); + res = nSize; + } else + res = reroutedSize; + memcpy(lpFilename, reroute.fileName(), reroutedSize * sizeof(lpFilename[0])); + lpFilename[reroutedSize] = 0; + + LOG_CALL() + .PARAM(hModule) + .addParam("lpFilename", (res != 0UL) ? lpFilename : L"") + .PARAM(nSize) + .PARAM(res) + .PARAM(callContext.lastError()); + } + } + HOOK_END + + return res; +} + +HANDLE WINAPI usvfs::hook_FindFirstFileExW( + LPCWSTR lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, LPVOID lpFindFileData, + FINDEX_SEARCH_OPS fSearchOp, LPVOID lpSearchFilter, DWORD dwAdditionalFlags) +{ + HANDLE res = INVALID_HANDLE_VALUE; + + HOOK_START_GROUP(MutExHookGroup::SEARCH_FILES) + if (!callContext.active()) { + res = FindFirstFileExW(lpFileName, fInfoLevelId, lpFindFileData, fSearchOp, + lpSearchFilter, dwAdditionalFlags); + callContext.updateLastError(); + return res; + } + + // FindFirstFileEx() must fail early if the path ends with a slash + if (lpFileName) { + const auto len = wcslen(lpFileName); + if (len > 0) { + if (lpFileName[len - 1] == L'\\' || lpFileName[len - 1] == L'/') { + spdlog::get("usvfs")->warn( + "hook_FindFirstFileExW(): path '{}' ends with slash, always fails", + fs::path(lpFileName).string()); + return INVALID_HANDLE_VALUE; + } + } + } + + fs::path finalPath; + RerouteW reroute; + fs::path originalPath; + + bool usedRewrite = false; + + // We need to do some trickery here, since we only want to use the hooked + // NtQueryDirectoryFile for rerouted locations we need to check if the Directory path + // has been routed instead of the full path. + originalPath = RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)); + PRE_REALCALL + res = ::FindFirstFileExW(originalPath.c_str(), fInfoLevelId, lpFindFileData, + fSearchOp, lpSearchFilter, dwAdditionalFlags); + POST_REALCALL + + if (res == INVALID_HANDLE_VALUE) { + fs::path searchPath = originalPath.filename(); + fs::path parentPath = originalPath.parent_path(); + std::wstring findPath = parentPath.wstring(); + while (findPath.find(L"*?<>\"", 0, 1) != std::wstring::npos) { + searchPath = parentPath.filename() / searchPath; + parentPath = parentPath.parent_path(); + findPath = parentPath.wstring(); + } + reroute = RerouteW::create(READ_CONTEXT(), callContext, parentPath.c_str()); + if (reroute.wasRerouted()) { + finalPath = reroute.fileName(); + finalPath /= searchPath.wstring(); + } + if (!finalPath.empty()) { + PRE_REALCALL + usedRewrite = true; + res = ::FindFirstFileExW(finalPath.c_str(), fInfoLevelId, lpFindFileData, + fSearchOp, lpSearchFilter, dwAdditionalFlags); + POST_REALCALL + } + } + + if (res != INVALID_HANDLE_VALUE) { + // store the original search path for use during iteration + WRITE_CONTEXT()->customData(SearchHandles)[res] = lpFileName; + } + + LOG_CALL() + .PARAM(lpFileName) + .PARAM(originalPath.c_str()) + .PARAM(res) + .PARAM(callContext.lastError()); + if (usedRewrite) + LOG_CALL() + .PARAM(lpFileName) + .PARAM(finalPath.c_str()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +HRESULT(WINAPI* usvfs::CopyFile2)(PCWSTR pwszExistingFileName, PCWSTR pwszNewFileName, + COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters); + +HRESULT WINAPI usvfs::hook_CopyFile2(PCWSTR pwszExistingFileName, + PCWSTR pwszNewFileName, + COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters) +{ + HRESULT res = E_FAIL; + + typedef HRESULT(WINAPI * CopyFile2_t)(PCWSTR, PCWSTR, COPYFILE2_EXTENDED_PARAMETERS*); + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = CopyFile2(pwszExistingFileName, pwszNewFileName, pExtendedParameters); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + CreateRerouter writeReroute; + bool callOriginal = true; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, pwszExistingFileName); + callOriginal = writeReroute.rerouteNew( + context, callContext, pwszNewFileName, + pExtendedParameters && + (pExtendedParameters->dwCopyFlags & COPY_FILE_FAIL_IF_EXISTS) == 0, + "hook_CopyFile2"); + } + + if (callOriginal) { + PRE_REALCALL + res = + CopyFile2(readReroute.fileName(), writeReroute.fileName(), pExtendedParameters); + POST_REALCALL + writeReroute.updateResult(callContext, SUCCEEDED(res)); + + if (SUCCEEDED(res) && writeReroute.newReroute()) + writeReroute.insertMapping(WRITE_CONTEXT()); + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetPrivateProfileStringA(LPCSTR lpAppName, LPCSTR lpKeyName, + LPCSTR lpDefault, + LPSTR lpReturnedString, DWORD nSize, + LPCSTR lpFileName) +{ + DWORD res = 0; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::GetPrivateProfileStringA(lpAppName, lpKeyName, lpDefault, lpReturnedString, + nSize, lpFileName); + callContext.updateLastError(); + return res; + } + + RerouteW reroute = RerouteW::create( + READ_CONTEXT(), callContext, ush::string_cast(lpFileName).c_str()); + + PRE_REALCALL + res = ::GetPrivateProfileStringA( + lpAppName, lpKeyName, lpDefault, lpReturnedString, nSize, + ush::string_cast(reroute.fileName()).c_str()); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpKeyName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetPrivateProfileStringW(LPCWSTR lpAppName, LPCWSTR lpKeyName, + LPCWSTR lpDefault, + LPWSTR lpReturnedString, DWORD nSize, + LPCWSTR lpFileName) +{ + DWORD res = 0; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::GetPrivateProfileStringW(lpAppName, lpKeyName, lpDefault, lpReturnedString, + nSize, lpFileName); + callContext.updateLastError(); + return res; + } + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName); + + PRE_REALCALL + res = ::GetPrivateProfileStringW(lpAppName, lpKeyName, lpDefault, lpReturnedString, + nSize, reroute.fileName()); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpKeyName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetPrivateProfileSectionA(LPCSTR lpAppName, + LPSTR lpReturnedString, DWORD nSize, + LPCSTR lpFileName) +{ + DWORD res = 0; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::GetPrivateProfileSectionA(lpAppName, lpReturnedString, nSize, lpFileName); + callContext.updateLastError(); + return res; + } + + RerouteW reroute = RerouteW::create( + READ_CONTEXT(), callContext, ush::string_cast(lpFileName).c_str()); + + PRE_REALCALL + res = ::GetPrivateProfileSectionA( + lpAppName, lpReturnedString, nSize, + ush::string_cast(reroute.fileName()).c_str()); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetPrivateProfileSectionW(LPCWSTR lpAppName, + LPWSTR lpReturnedString, DWORD nSize, + LPCWSTR lpFileName) +{ + DWORD res = 0; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::GetPrivateProfileSectionW(lpAppName, lpReturnedString, nSize, lpFileName); + callContext.updateLastError(); + return res; + } + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName); + + PRE_REALCALL + res = ::GetPrivateProfileSectionW(lpAppName, lpReturnedString, nSize, + reroute.fileName()); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_WritePrivateProfileStringA(LPCSTR lpAppName, LPCSTR lpKeyName, + LPCSTR lpString, LPCSTR lpFileName) +{ + BOOL res = false; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::WritePrivateProfileStringA(lpAppName, lpKeyName, lpString, lpFileName); + callContext.updateLastError(); + return res; + } + + CreateRerouter reroute; + bool callOriginal = reroute.rerouteNew( + READ_CONTEXT(), callContext, ush::string_cast(lpFileName).c_str(), + true, "hook_WritePrivateProfileStringA"); + + if (callOriginal) { + PRE_REALCALL + res = ::WritePrivateProfileStringA( + lpAppName, lpKeyName, lpString, + ush::string_cast(reroute.fileName()).c_str()); + POST_REALCALL + reroute.updateResult(callContext, res); + + if (res && reroute.newReroute()) + reroute.insertMapping(WRITE_CONTEXT()); + + if (reroute.wasRerouted() || reroute.changedError()) + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpKeyName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(reroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_WritePrivateProfileStringW(LPCWSTR lpAppName, LPCWSTR lpKeyName, + LPCWSTR lpString, LPCWSTR lpFileName) +{ + BOOL res = false; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::WritePrivateProfileStringW(lpAppName, lpKeyName, lpString, lpFileName); + callContext.updateLastError(); + return res; + } + + CreateRerouter reroute; + bool callOriginal = reroute.rerouteNew(READ_CONTEXT(), callContext, lpFileName, true, + "hook_WritePrivateProfileStringW"); + + if (callOriginal) { + PRE_REALCALL + res = ::WritePrivateProfileStringW(lpAppName, lpKeyName, lpString, + reroute.fileName()); + POST_REALCALL + reroute.updateResult(callContext, res); + + if (res && reroute.newReroute()) + reroute.insertMapping(WRITE_CONTEXT()); + + if (reroute.wasRerouted() || reroute.changedError()) + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpKeyName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(reroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +VOID WINAPI usvfs::hook_ExitProcess(UINT exitCode) +{ + HOOK_START + + { + HookContext::Ptr context = WRITE_CONTEXT(); + + std::vector>& delayed = context->delayed(); + + if (!delayed.empty()) { + // ensure all delayed tasks are completed before we exit the process + for (std::future& delayedOp : delayed) { + delayedOp.get(); + } + delayed.clear(); + } + } + + // exitprocess doesn't return so logging the call after the real call doesn't + // make much sense. + // nor does any pre/post call macro + LOG_CALL().PARAM(exitCode); + + usvfsDisconnectVFS(); + + // HookManager::instance().removeHook("ExitProcess"); + // PRE_REALCALL + ::ExitProcess(exitCode); + // POST_REALCALL + + HOOK_END +} diff --git a/libs/usvfs/src/usvfs_dll/hooks/kernel32.h b/libs/usvfs/src/usvfs_dll/hooks/kernel32.h new file mode 100644 index 0000000..7087b2e --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/kernel32.h @@ -0,0 +1,116 @@ +#pragma once + +#include "ntdll_declarations.h" +#include + +namespace usvfs +{ + +DLLEXPORT BOOL WINAPI hook_GetFileAttributesExA(LPCSTR lpFileName, + GET_FILEEX_INFO_LEVELS fInfoLevelId, + LPVOID lpFileInformation); +DLLEXPORT BOOL WINAPI hook_GetFileAttributesExW(LPCWSTR lpFileName, + GET_FILEEX_INFO_LEVELS fInfoLevelId, + LPVOID lpFileInformation); +DLLEXPORT DWORD WINAPI hook_GetFileAttributesA(LPCSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_GetFileAttributesW(LPCWSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_SetFileAttributesW(LPCWSTR lpFileName, + DWORD dwFileAttributes); + +DLLEXPORT DWORD WINAPI hook_GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer); +DLLEXPORT DWORD WINAPI hook_GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer); +DLLEXPORT BOOL WINAPI hook_SetCurrentDirectoryA(LPCSTR lpPathName); +DLLEXPORT BOOL WINAPI hook_SetCurrentDirectoryW(LPCWSTR lpPathName); + +DLLEXPORT DWORD WINAPI hook_GetFullPathNameA(LPCSTR lpFileName, DWORD nBufferLength, + LPSTR lpBuffer, LPSTR* lpFilePart); +DLLEXPORT DWORD WINAPI hook_GetFullPathNameW(LPCWSTR lpFileName, DWORD nBufferLength, + LPWSTR lpBuffer, LPWSTR* lpFilePart); + +DLLEXPORT BOOL WINAPI hook_CreateDirectoryW(LPCWSTR lpPathName, + LPSECURITY_ATTRIBUTES lpSecurityAttributes); +DLLEXPORT BOOL WINAPI hook_RemoveDirectoryW(LPCWSTR lpPathName); + +DLLEXPORT BOOL WINAPI hook_DeleteFileW(LPCWSTR lpFileName); + +DLLEXPORT BOOL WINAPI hook_MoveFileA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName); +DLLEXPORT BOOL WINAPI hook_MoveFileW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName); +DLLEXPORT BOOL WINAPI hook_MoveFileExA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName, + DWORD dwFlags); +DLLEXPORT BOOL WINAPI hook_MoveFileExW(LPCWSTR lpExistingFileName, + LPCWSTR lpNewFileName, DWORD dwFlags); +DLLEXPORT BOOL WINAPI hook_MoveFileWithProgressA(LPCSTR lpExistingFileName, + LPCSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, DWORD dwFlags); +DLLEXPORT BOOL WINAPI hook_MoveFileWithProgressW(LPCWSTR lpExistingFileName, + LPCWSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, DWORD dwFlags); +; + +DLLEXPORT BOOL WINAPI hook_CopyFileExW(LPCWSTR lpExistingFileName, + LPCWSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, LPBOOL pbCancel, + DWORD dwCopyFlags); + +extern HRESULT(WINAPI* CopyFile2)(PCWSTR pwszExistingFileName, PCWSTR pwszNewFileName, + COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters); +DLLEXPORT HRESULT WINAPI +hook_CopyFile2(PCWSTR pwszExistingFileName, PCWSTR pwszNewFileName, + COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters); + +DLLEXPORT HMODULE WINAPI hook_LoadLibraryExA(LPCSTR lpFileName, HANDLE hFile, + DWORD dwFlags); +DLLEXPORT HMODULE WINAPI hook_LoadLibraryExW(LPCWSTR lpFileName, HANDLE hFile, + DWORD dwFlags); + +extern BOOL(WINAPI* CreateProcessInternalW)( + LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken); +DLLEXPORT BOOL WINAPI hook_CreateProcessInternalW( + LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken); + +DLLEXPORT DWORD WINAPI hook_GetModuleFileNameA(HMODULE hModule, LPSTR lpFilename, + DWORD nSize); +DLLEXPORT DWORD WINAPI hook_GetModuleFileNameW(HMODULE hModule, LPWSTR lpFilename, + DWORD nSize); + +DLLEXPORT HANDLE WINAPI hook_FindFirstFileExW( + LPCWSTR lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, LPVOID lpFindFileData, + FINDEX_SEARCH_OPS fSearchOp, LPVOID lpSearchFilter, DWORD dwAdditionalFlags); + +DLLEXPORT DWORD WINAPI hook_GetPrivateProfileStringA(LPCSTR lpAppName, LPCSTR lpKeyName, + LPCSTR lpDefault, + LPSTR lpReturnedString, + DWORD nSize, LPCSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_GetPrivateProfileStringW(LPCWSTR lpAppName, + LPCWSTR lpKeyName, + LPCWSTR lpDefault, + LPWSTR lpReturnedString, + DWORD nSize, LPCWSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_GetPrivateProfileSectionA(LPCSTR lpAppName, + LPSTR lpReturnedString, + DWORD nSize, LPCSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_GetPrivateProfileSectionW(LPCWSTR lpAppName, + LPWSTR lpReturnedString, + DWORD nSize, LPCWSTR lpFileName); +DLLEXPORT BOOL WINAPI hook_WritePrivateProfileStringA(LPCSTR lpAppName, + LPCSTR lpKeyName, LPCSTR lpString, + LPCSTR lpFileName); +DLLEXPORT BOOL WINAPI hook_WritePrivateProfileStringW(LPCWSTR lpAppName, + LPCWSTR lpKeyName, + LPCWSTR lpString, + LPCWSTR lpFileName); + +DLLEXPORT VOID WINAPI hook_ExitProcess(UINT exitCode); + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp b/libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp new file mode 100644 index 0000000..ad53418 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp @@ -0,0 +1,1520 @@ +#include "ntdll.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include "../hookcallcontext.h" +#include "../hookcontext.h" +#include "../maptracker.h" +#include "../stringcast_boost.h" + +#include "file_information_utils.h" +#include "sharedids.h" + +namespace ulog = usvfs::log; +namespace ush = usvfs::shared; +namespace bfs = boost::filesystem; + +using usvfs::UnicodeString; + +// flag definitions below are copied from winternl.h +#define FILE_SUPERSEDE 0x00000000 +#define FILE_OPEN 0x00000001 +#define FILE_CREATE 0x00000002 +#define FILE_OPEN_IF 0x00000003 +#define FILE_OVERWRITE 0x00000004 +#define FILE_OVERWRITE_IF 0x00000005 +#define FILE_MAXIMUM_DISPOSITION 0x00000005 + +template +using unique_ptr_deleter = std::unique_ptr; + +class RedirectionInfo +{ +public: + UnicodeString path; + bool redirected; + + RedirectionInfo() {} + RedirectionInfo(UnicodeString path, bool redirected) + : path(path), redirected(redirected) + {} +}; + +class HandleTracker +{ +public: + using handle_type = HANDLE; + using info_type = UnicodeString; + + HandleTracker() { insert_current_directory(); } + + info_type lookup(handle_type handle) const + { + if (valid_handle(handle)) { + std::shared_lock lock(m_mutex); + auto find = m_map.find(handle); + if (find != m_map.end()) + return find->second; + } + return info_type(); + } + + void insert(handle_type handle, const info_type& info) + { + if (!valid_handle(handle)) + return; + std::unique_lock lock(m_mutex); + m_map[handle] = info; + } + + void erase(handle_type handle) + { + if (!valid_handle(handle)) + return; + std::unique_lock lock(m_mutex); + m_map.erase(handle); + } + +private: + static bool valid_handle(handle_type handle) + { + return handle && handle != INVALID_HANDLE_VALUE; + } + + void insert_current_directory() + { + ntdll_declarations_init(); + + UNICODE_STRING p{0}; + RTL_RELATIVE_NAME r{0}; + NTSTATUS status = + RtlDosPathNameToRelativeNtPathName_U_WithStatus(L"x", &p, nullptr, &r); + if (status >= 0) { + if (r.ContainingDirectory && p.Buffer) { + size_t len = p.Length / sizeof(WCHAR); + size_t trim = strlen("\\x") + (p.Buffer[len - 1] ? 0 : 1); + if (len > trim) + insert(r.ContainingDirectory, info_type(p.Buffer, len - trim)); + } + RtlReleaseRelativeName(&r); + if (p.Buffer) + HeapFree(GetProcessHeap(), 0, p.Buffer); + } + } + + mutable std::shared_mutex m_mutex; + std::unordered_map m_map; +}; + +HandleTracker ntdllHandleTracker; + +UnicodeString CreateUnicodeString(const OBJECT_ATTRIBUTES* objectAttributes) +{ + UnicodeString result = ntdllHandleTracker.lookup(objectAttributes->RootDirectory); + if (objectAttributes->ObjectName != nullptr) { + result.appendPath(objectAttributes->ObjectName); + } + return result; +} + +std::ostream& operator<<(std::ostream& os, const _UNICODE_STRING& str) +{ + try { + // TODO this does not correctly support surrogate pairs + // since the size used here is the number of 16-bit characters in the buffer + // whereas + // toNarrow expects the actual number of characters. + // It will always underestimate though, so worst case scenario we truncate + // the string + os << ush::string_cast(str.Buffer, ush::CodePage::UTF8, + str.Length / sizeof(WCHAR)); + } catch (const std::exception& e) { + os << e.what(); + } + + return os; +} + +std::ostream& operator<<(std::ostream& os, POBJECT_ATTRIBUTES attr) +{ + return operator<<(os, *attr->ObjectName); +} + +RedirectionInfo applyReroute(const usvfs::HookContext::ConstPtr& context, + const usvfs::HookCallContext& callContext, + const UnicodeString& inPath) +{ + RedirectionInfo result; + result.path = inPath; + result.redirected = false; + + if (callContext.active()) { + // see if the file exists in the redirection tree + std::string lookupPath = ush::string_cast( + static_cast(result.path) + 4, ush::CodePage::UTF8); + auto node = context->redirectionTable()->findNode(lookupPath.c_str()); + // if so, replace the file name with the path to the mapped file + if ((node.get() != nullptr) && + (!node->data().linkTarget.empty() || node->isDirectory())) { + std::wstring reroutePath; + + if (node->data().linkTarget.length() > 0) { + reroutePath = ush::string_cast(node->data().linkTarget.c_str(), + ush::CodePage::UTF8); + } else { + reroutePath = + ush::string_cast(node->path().c_str(), ush::CodePage::UTF8); + } + if ((*reroutePath.rbegin() == L'\\') && (*lookupPath.rbegin() != '\\')) { + reroutePath.resize(reroutePath.size() - 1); + } + std::replace(reroutePath.begin(), reroutePath.end(), L'/', L'\\'); + if (reroutePath[1] == L'\\') + reroutePath[1] = L'?'; + result.path = LR"(\??\)" + reroutePath; + result.redirected = true; + } + } + return result; +} + +RedirectionInfo applyReroute(const usvfs::CreateRerouter& rerouter) +{ + RedirectionInfo result; + result.path = rerouter.fileName(); + result.redirected = rerouter.wasRerouted(); + + std::wstring reroutePath(static_cast(result.path)); + std::replace(reroutePath.begin(), reroutePath.end(), L'/', L'\\'); + if (reroutePath[1] == L'\\') + reroutePath[1] = L'?'; + if (!((reroutePath[0] == L'\\') && (reroutePath[1] == L'?') && + (reroutePath[2] == L'?') && (reroutePath[3] == L'\\'))) { + result.path = LR"(\??\)" + reroutePath; + } + + return result; +} + +struct FindCreateTarget +{ + usvfs::RedirectionTree::NodePtrT target; + void operator()(usvfs::RedirectionTree::NodePtrT node) + { + if (node->hasFlag(usvfs::shared::FLAG_CREATETARGET)) { + target = node; + } + } +}; + +std::pair +findCreateTarget(const usvfs::HookContext::ConstPtr& context, + const UnicodeString& inPath) +{ + std::pair result; + result.first = inPath; + result.second = UnicodeString(); + + std::string lookupPath = ush::string_cast( + static_cast(result.first) + 4, ush::CodePage::UTF8); + FindCreateTarget visitor; + usvfs::RedirectionTree::VisitorFunction visitorWrapper = + [&](const usvfs::RedirectionTree::NodePtrT& node) { + visitor(node); + }; + context->redirectionTable()->visitPath(lookupPath, visitorWrapper); + if (visitor.target.get() != nullptr) { + bfs::path relativePath = + ush::make_relative(visitor.target->path(), bfs::path(lookupPath)); + + bfs::path target(visitor.target->data().linkTarget.c_str()); + target /= relativePath; + + result.second = UnicodeString(target.wstring().c_str()); + winapi::ex::wide::createPath(target.parent_path()); + } + return result; +} + +RedirectionInfo applyReroute(const usvfs::HookContext::ConstPtr& context, + const usvfs::HookCallContext& callContext, + POBJECT_ATTRIBUTES inAttributes) +{ + return applyReroute(context, callContext, CreateUnicodeString(inAttributes)); +} + +int NextDividableBy(int number, int divider) +{ + return static_cast( + ceilf(static_cast(number) / static_cast(divider)) * divider); +} + +// Something is trying to create a variety of files starting with "\Device\", +// such as "\Device\DeviceApi\Dev\Query", "\Device\MMCSS\MmThread", +// "\Device\DeviceApi\CMNotify", etc. +// +// This used to create a bunch of spurious "Device" and "MMCSS" folders when +// using Explorer++. Some of these names seem to part of PnP, others from the +// "Multimedia Class Scheduler Service". +// +// There's basically zero information on this stuff online. +// +// Regardless, these files ended up being rerouted, so they became something +// like C:\game\Data\Somewhere\Device\MMCSS\MmThread, which ended up being +// rerouted to overwrite and created as a fake path +// "overwrite\Somewhere\Device\MMCSS" +// +// These paths are weird, they feel like pipes or something. It's not clear how +// they should be reliably recognized, so this is a hardcoded check for any +// path that starts with "\Device\". These paths will be forwarded directly +// to NtCreateFile/NtOpenFile +// +bool isDeviceFile(std::wstring_view name) +{ + static const std::wstring_view DevicePrefix(L"\\Device\\"); + + // starts with + if (name.size() < DevicePrefix.size()) { + return false; + } else { + return (_wcsnicmp(name.data(), DevicePrefix.data(), DevicePrefix.size()) == 0); + } +} + +NTSTATUS addNtSearchData(HANDLE hdl, PUNICODE_STRING FileName, + const std::wstring& fakeName, + FILE_INFORMATION_CLASS FileInformationClass, PVOID& buffer, + ULONG& bufferSize, std::set& foundFiles, + HANDLE event, PIO_APC_ROUTINE apcRoutine, PVOID apcContext, + BOOLEAN returnSingleEntry) +{ + NTSTATUS res = STATUS_NO_SUCH_FILE; + if (hdl != INVALID_HANDLE_VALUE) { + PVOID lastValidRecord = nullptr; + PVOID bufferInit = buffer; + IO_STATUS_BLOCK status; + res = NtQueryDirectoryFile(hdl, event, apcRoutine, apcContext, &status, buffer, + bufferSize, FileInformationClass, returnSingleEntry, + FileName, FALSE); + + if ((res != STATUS_SUCCESS) || (status.Information <= 0)) { + bufferSize = 0UL; + } else { + ULONG totalOffset = 0; + PVOID lastSkipPos = nullptr; + + while (totalOffset < status.Information) { + ULONG offset; + std::wstring fileName; + GetFileInformationData(FileInformationClass, buffer, offset, fileName); + // in case this is a single-file search result and the specified + // filename differs from the file name found, replace it in the + // information structure + if ((totalOffset == 0) && (offset == 0) && (fakeName.length() > 0)) { + // if the fake name is larger than what is in the buffer and there is + // not enough room, that's a buffer overflow + if ((fakeName.length() > fileName.length()) && + ((fakeName.length() - fileName.length()) > + (bufferSize - status.Information))) { + res = STATUS_BUFFER_OVERFLOW; + break; + } + // WARNING for the case where the fake name is longer this needs to + // move back all further results and update the offset first + SetFileInformationFileName(FileInformationClass, buffer, fakeName); + fileName = fakeName; + } + bool add = true; + if (fileName.length() > 0) { + auto insertRes = foundFiles.insert(ush::to_upper(fileName)); + add = insertRes.second; // add only if we didn't find this file before + } + if (!add) { + if (lastSkipPos == nullptr) { + lastSkipPos = buffer; + } + } else { + if (lastSkipPos != nullptr) { + memmove(lastSkipPos, buffer, status.Information - totalOffset); + ULONG delta = static_cast(ush::AddrDiff(buffer, lastSkipPos)); + totalOffset -= delta; + + buffer = lastSkipPos; + lastSkipPos = nullptr; + } + lastValidRecord = buffer; + } + + if (offset == 0) { + offset = static_cast(status.Information) - totalOffset; + } + buffer = ush::AddrAdd(buffer, offset); + totalOffset += offset; + } + + if (lastSkipPos != nullptr) { + buffer = lastSkipPos; + bufferSize = static_cast(ush::AddrDiff(buffer, bufferInit)); + // null out the unused rest if there is some + memset(lastSkipPos, 0, status.Information - bufferSize); + } else { + bufferSize = static_cast(ush::AddrDiff(buffer, bufferInit)); + } + } + if (lastValidRecord != nullptr) { + SetFileInformationOffset(FileInformationClass, lastValidRecord, 0); + } + } + return res; +} + +DATA_ID(SearchInfo); + +struct Searches +{ + struct Info + { + struct VirtualMatch + { + // full path to where the file/directory actually is + std::wstring realPath; + // virtual filename (only filename since it has to be within the searched + // directory) + // this is left empty when a folder with all its content is mapped to the + // search directory + std::wstring virtualName; + }; + + Info() : currentSearchHandle(INVALID_HANDLE_VALUE) {} + std::set foundFiles; + HANDLE currentSearchHandle; + std::queue virtualMatches; + UnicodeString searchPattern; + bool regularComplete{false}; + }; + + Searches() = default; + + // must provide a special copy constructor because boost::mutex is + // non-copyable + Searches(const Searches& reference) : info(reference.info) {} + + Searches& operator=(const Searches&) = delete; + + std::recursive_mutex queryMutex; + + std::map info; +}; + +void gatherVirtualEntries(const UnicodeString& dirName, + const usvfs::RedirectionTreeContainer& redir, + PUNICODE_STRING FileName, Searches::Info& info) +{ + LPCWSTR dirNameW = static_cast(dirName); + // fix directory name. I'd love to know why microsoft sometimes uses "\??\" vs + // "\\?\" + if ((wcsncmp(dirNameW, LR"(\\?\)", 4) == 0) || + (wcsncmp(dirNameW, LR"(\??\)", 4) == 0)) { + dirNameW += 4; + } + auto node = redir->findNode(boost::filesystem::path(dirNameW)); + if (node.get() != nullptr) { + std::string searchPattern = + FileName != nullptr + ? ush::string_cast(FileName->Buffer, ush::CodePage::UTF8) + : "*.*"; + + boost::replace_all(searchPattern, "\"", "."); + + for (const auto& subNode : node->find(searchPattern)) { + if (((subNode->data().linkTarget.length() > 0) || subNode->isDirectory()) && + !subNode->hasFlag(usvfs::shared::FLAG_DUMMY)) { + std::wstring vName = + ush::string_cast(subNode->name(), ush::CodePage::UTF8); + + Searches::Info::VirtualMatch m; + if (subNode->data().linkTarget.length() > 0) { + m = {ush::string_cast(subNode->data().linkTarget.c_str(), + ush::CodePage::UTF8), + vName}; + } else { + m = {ush::string_cast(subNode->path().c_str(), + ush::CodePage::UTF8), + vName}; + } + + info.virtualMatches.push(m); + info.foundFiles.insert(ush::to_upper(vName)); + } + } + } +} + +/** + * @brief insert a virtual entry into the search result + * @param FileInformation + * @param FileInformationClass + * @param info + * @param realPath path were the actual file resides + * @param virtualName virtual file name (without path). will often be the same + * as the name component of realpath + * @param ReturnSingleEntry + * @param dataRead + * @return true if a virtual result was added, false if the search handle in the + * info object yields no more results + */ +bool addVirtualSearchResult(PVOID& FileInformation, + FILE_INFORMATION_CLASS FileInformationClass, + Searches::Info& info, const std::wstring& realPath, + const std::wstring& virtualName, BOOLEAN ReturnSingleEntry, + ULONG& dataRead) +{ + // this opens a search in the real location, then copies the information about + // files we care about (the ones being mapped) to the result we intend to + // return + bfs::path fullPath(realPath); + if (fullPath.filename().wstring() == L".") { + fullPath = fullPath.parent_path(); + } + if (info.currentSearchHandle == INVALID_HANDLE_VALUE) { + std::wstring dirName = fullPath.parent_path().wstring(); + if (dirName.length() >= MAX_PATH && !ush::startswith(dirName.c_str(), LR"(\\?\)")) + dirName = LR"(\\?\)" + dirName; + info.currentSearchHandle = + CreateFileW(dirName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } + std::wstring fileName = + ush::string_cast(fullPath.filename().string(), ush::CodePage::UTF8); + NTSTATUS subRes = addNtSearchData( + info.currentSearchHandle, + (fileName != L".") ? static_cast(UnicodeString(fileName.c_str())) + : nullptr, + virtualName, FileInformationClass, FileInformation, dataRead, info.foundFiles, + nullptr, nullptr, nullptr, ReturnSingleEntry); + if (subRes == STATUS_SUCCESS) { + return true; + } else { + // STATUS_NO_MORE_FILES merely means the search ended, everything else is an + // error message. Either way, the search here is finished and we should + // resume in the next mapped directory + if (subRes != STATUS_NO_MORE_FILES) { + spdlog::get("hooks")->warn("error reported listing files in {0}: {1:x}", + fullPath.string(), static_cast(subRes)); + } + return false; + } +} + +NTSTATUS WINAPI usvfs::hook_NtQueryDirectoryFile( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, BOOLEAN ReturnSingleEntry, + PUNICODE_STRING FileName, BOOLEAN RestartScan) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + // this is quite messy... + // first, this will gather the virtual locations mapping to the iterated one + // then we return results from the real location, skipping those that exist + // in the virtual locations, as those take precedence + // finally the virtual results are returned, adding each result to a skip + // list, so they don't get added twice + // + // if we don't add the regular files first, "." and ".." wouldn't be in the + // first search result of wildcard searches which may confuse the caller + NTSTATUS res = STATUS_NO_MORE_FILES; + HOOK_START_GROUP(MutExHookGroup::FIND_FILES) + if (!callContext.active()) { + return ::NtQueryDirectoryFile( + FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, FileInformation, + Length, FileInformationClass, ReturnSingleEntry, FileName, RestartScan); + } + + // std::unique_lock queryLock; + std::map::iterator infoIter; + bool firstSearch = false; + + { // scope to limit context lifetime + HookContext::ConstPtr context = READ_CONTEXT(); + Searches& activeSearches = context->customData(SearchInfo); + // queryLock = std::unique_lock(activeSearches.queryMutex); + + if (RestartScan) { + auto iter = activeSearches.info.find(FileHandle); + if (iter != activeSearches.info.end()) { + activeSearches.info.erase(iter); + } + } + + // see if we already have a running search + infoIter = activeSearches.info.find(FileHandle); + firstSearch = (infoIter == activeSearches.info.end()); + } + + if (firstSearch) { + HookContext::Ptr context = WRITE_CONTEXT(); + Searches& activeSearches = context->customData(SearchInfo); + // tradeoff time: we store this search status even if no virtual results + // were found. This causes a little extra cost here and in NtClose every + // time a non-virtual dir is being searched. However if we don't, + // whenever NtQueryDirectoryFile is called another time on the same handle, + // this (expensive) block would be run again. + infoIter = + activeSearches.info.insert(std::make_pair(FileHandle, Searches::Info())).first; + infoIter->second.searchPattern.appendPath(FileName); + + SearchHandleMap& searchMap = context->customData(SearchHandles); + SearchHandleMap::iterator iter = searchMap.find(FileHandle); + + UnicodeString searchPath; + if (iter != searchMap.end()) { + searchPath = UnicodeString(iter->second.c_str()); + infoIter->second.currentSearchHandle = CreateFileW( + iter->second.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } else { + searchPath = ntdllHandleTracker.lookup(FileHandle); + } + gatherVirtualEntries(searchPath, context->redirectionTable(), FileName, + infoIter->second); + } + + ULONG dataRead = Length; + PVOID FileInformationCurrent = FileInformation; + + // add regular search results, skipping those files we have in a virtual + // location + bool moreRegular = !infoIter->second.regularComplete; + bool dataReturned = false; + while (moreRegular && !dataReturned) { + dataRead = Length; + + HANDLE handle = infoIter->second.currentSearchHandle; + if (handle == INVALID_HANDLE_VALUE) { + handle = FileHandle; + } + NTSTATUS subRes = addNtSearchData( + handle, FileName, L"", FileInformationClass, FileInformationCurrent, dataRead, + infoIter->second.foundFiles, Event, ApcRoutine, ApcContext, ReturnSingleEntry); + moreRegular = subRes == STATUS_SUCCESS; + if (moreRegular) { + dataReturned = dataRead != 0; + } else { + infoIter->second.regularComplete = true; + infoIter->second.foundFiles.clear(); + if (infoIter->second.currentSearchHandle != INVALID_HANDLE_VALUE) { + ::CloseHandle(infoIter->second.currentSearchHandle); + infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE; + } + } + } + if (!moreRegular) { + // add virtual results + while (!dataReturned && infoIter->second.virtualMatches.size() > 0) { + auto match = infoIter->second.virtualMatches.front(); + if (match.realPath.size() != 0) { + dataRead = Length; + if (addVirtualSearchResult(FileInformationCurrent, FileInformationClass, + infoIter->second, match.realPath, match.virtualName, + ReturnSingleEntry, dataRead)) { + // a positive result here means the call returned data and there may + // be further objects to be retrieved by repeating the call + dataReturned = true; + } else { + // proceed to next search handle + + // TODO: doesn't append search results from more than one redirection + // per call. This is bad for performance but otherwise we'd need to + // re-write the offsets between information objects + infoIter->second.virtualMatches.pop(); + CloseHandle(infoIter->second.currentSearchHandle); + infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE; + } + } + } + } + + if (!dataReturned) { + if (firstSearch) { + res = STATUS_NO_SUCH_FILE; + } else { + res = STATUS_NO_MORE_FILES; + } + } else { + res = STATUS_SUCCESS; + } + IoStatusBlock->Status = res; + IoStatusBlock->Information = dataRead; + + size_t numVirtualFiles = infoIter->second.virtualMatches.size(); + if ((numVirtualFiles > 0)) { + LOG_CALL() + .addParam("path", ntdllHandleTracker.lookup(FileHandle)) + .PARAM(FileInformationClass) + .PARAM(FileName) + .PARAM(numVirtualFiles) + .PARAMWRAP(res); + } + + HOOK_END + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtQueryDirectoryFileEx( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, ULONG QueryFlags, + PUNICODE_STRING FileName) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + NTSTATUS res = STATUS_NO_MORE_FILES; + + // this is quite messy... + // first, this will gather the virtual locations mapping to the iterated one + // then we return results from the real location, skipping those that exist + // in the virtual locations, as those take precedence + // finally the virtual results are returned, adding each result to a skip + // list, so they don't get added twice + // + // if we don't add the regular files first, "." and ".." wouldn't be in the + // first search result of wildcard searches which may confuse the caller + HOOK_START_GROUP(MutExHookGroup::FIND_FILES) + if (!callContext.active()) { + return ::NtQueryDirectoryFileEx(FileHandle, Event, ApcRoutine, ApcContext, + IoStatusBlock, FileInformation, Length, + FileInformationClass, QueryFlags, FileName); + } + + // std::unique_lock queryLock; + std::map::iterator infoIter; + bool firstSearch = false; + + { // scope to limit context lifetime + HookContext::ConstPtr context = READ_CONTEXT(); + Searches& activeSearches = context->customData(SearchInfo); + // queryLock = std::unique_lock(activeSearches.queryMutex); + + if (QueryFlags & SL_RESTART_SCAN) { + auto iter = activeSearches.info.find(FileHandle); + if (iter != activeSearches.info.end()) { + activeSearches.info.erase(iter); + } + } + + // see if we already have a running search + infoIter = activeSearches.info.find(FileHandle); + firstSearch = (infoIter == activeSearches.info.end()); + } + + if (firstSearch) { + HookContext::Ptr context = WRITE_CONTEXT(); + Searches& activeSearches = context->customData(SearchInfo); + // tradeoff time: we store this search status even if no virtual results + // were found. This causes a little extra cost here and in NtClose every + // time a non-virtual dir is being searched. However if we don't, + // whenever NtQueryDirectoryFile is called another time on the same handle, + // this (expensive) block would be run again. + infoIter = + activeSearches.info.insert(std::make_pair(FileHandle, Searches::Info())).first; + infoIter->second.searchPattern.appendPath(FileName); + + SearchHandleMap& searchMap = context->customData(SearchHandles); + SearchHandleMap::iterator iter = searchMap.find(FileHandle); + + UnicodeString searchPath; + if (iter != searchMap.end()) { + searchPath = UnicodeString(iter->second.c_str()); + infoIter->second.currentSearchHandle = CreateFileW( + iter->second.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } else { + searchPath = ntdllHandleTracker.lookup(FileHandle); + } + gatherVirtualEntries(searchPath, context->redirectionTable(), FileName, + infoIter->second); + } + + ULONG dataRead = Length; + PVOID FileInformationCurrent = FileInformation; + + // add regular search results, skipping those files we have in a virtual + // location + bool moreRegular = !infoIter->second.regularComplete; + bool dataReturned = false; + while (moreRegular && !dataReturned) { + dataRead = Length; + + HANDLE handle = infoIter->second.currentSearchHandle; + if (handle == INVALID_HANDLE_VALUE) { + handle = FileHandle; + } + NTSTATUS subRes = addNtSearchData(handle, FileName, L"", FileInformationClass, + FileInformationCurrent, dataRead, + infoIter->second.foundFiles, Event, ApcRoutine, + ApcContext, QueryFlags & SL_RETURN_SINGLE_ENTRY); + moreRegular = subRes == STATUS_SUCCESS; + if (moreRegular) { + dataReturned = dataRead != 0; + } else { + infoIter->second.regularComplete = true; + infoIter->second.foundFiles.clear(); + if (infoIter->second.currentSearchHandle != INVALID_HANDLE_VALUE) { + ::CloseHandle(infoIter->second.currentSearchHandle); + infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE; + } + } + } + if (!moreRegular) { + // add virtual results + while (!dataReturned && infoIter->second.virtualMatches.size() > 0) { + auto match = infoIter->second.virtualMatches.front(); + if (match.realPath.size() != 0) { + dataRead = Length; + if (addVirtualSearchResult(FileInformationCurrent, FileInformationClass, + infoIter->second, match.realPath, match.virtualName, + QueryFlags & SL_RETURN_SINGLE_ENTRY, dataRead)) { + // a positive result here means the call returned data and there may + // be further objects to be retrieved by repeating the call + dataReturned = true; + } else { + // proceed to next search handle + + // TODO: doesn't append search results from more than one redirection + // per call. This is bad for performance but otherwise we'd need to + // re-write the offsets between information objects + infoIter->second.virtualMatches.pop(); + CloseHandle(infoIter->second.currentSearchHandle); + infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE; + } + } + } + } + + if (!dataReturned) { + if (firstSearch) { + res = STATUS_NO_SUCH_FILE; + } else { + res = STATUS_NO_MORE_FILES; + } + } else { + res = STATUS_SUCCESS; + } + IoStatusBlock->Status = res; + IoStatusBlock->Information = dataRead; + + size_t numVirtualFiles = infoIter->second.virtualMatches.size(); + if ((numVirtualFiles > 0)) { + LOG_CALL() + .addParam("path", ntdllHandleTracker.lookup(FileHandle)) + .PARAM(FileInformationClass) + .PARAM(FileName) + .PARAM(QueryFlags) + .PARAM(numVirtualFiles) + .PARAMWRAP(res); + } + + HOOK_END + return res; +} + +DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryObject( + HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass, + PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength) +{ + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active()) { + return ::NtQueryObject(Handle, ObjectInformationClass, ObjectInformation, + ObjectInformationLength, ReturnLength); + } + + PRE_REALCALL + res = ::NtQueryObject(Handle, ObjectInformationClass, ObjectInformation, + ObjectInformationLength, ReturnLength); + POST_REALCALL + + // we handle both SUCCESS and BUFFER_OVERFLOW since the fixed name might be + // smaller than the original one + // + // we only handle STATUS_INFO_LENGTH_MISMATCH if ReturnLength is not NULL since + // this is only returned if the length is too small to hold the structure itself + // (regardless of the name), in which case, we need to compute our own ReturnLength + // + if ((res == STATUS_SUCCESS || res == STATUS_BUFFER_OVERFLOW || + (res == STATUS_INFO_LENGTH_MISMATCH && ReturnLength)) && + (ObjectInformationClass == ObjectNameInformation)) { + const auto trackerInfo = ntdllHandleTracker.lookup(Handle); + const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo); + + OBJECT_NAME_INFORMATION* info = + reinterpret_cast(ObjectInformation); + + if (redir.redirected) { + // https://learn.microsoft.com/en-us/windows/win32/fileio/displaying-volume-paths + // + + // TODO: is that always true? + // path should start with \??\X: - we need to replace this by device name + // + WCHAR deviceName[MAX_PATH]; + std::wstring buffer(static_cast(trackerInfo)); + buffer[6] = L'\0'; + + QueryDosDeviceW(buffer.data() + 4, deviceName, ARRAYSIZE(deviceName)); + + buffer = std::wstring(deviceName) + L'\\' + + std::wstring(buffer.data() + 7, buffer.size() - 7); + + // the name is put in the buffer AFTER the struct, so the required size if + // sizeof(OBJECT_NAME_INFORMATION) + the number of bytes for the name + 2 bytes + // for a wide null character + const auto requiredLength = + sizeof(OBJECT_NAME_INFORMATION) + buffer.size() * 2 + 2; + if (ObjectInformationLength < requiredLength) { + // if the status was info length mismatch, we keep it, we are just going to + // update *ReturnLength + if (res != STATUS_INFO_LENGTH_MISMATCH) { + res = STATUS_BUFFER_OVERFLOW; + } + + if (ReturnLength) { + *ReturnLength = static_cast(requiredLength); + } + } else { + // put the unicode buffer at the end of the object + const USHORT unicodeBufferLength = static_cast(std::min( + static_cast(std::numeric_limits::max()), + static_cast(ObjectInformationLength - + sizeof(OBJECT_NAME_INFORMATION)))); + LPWSTR unicodeBuffer = reinterpret_cast( + static_cast(ObjectInformation) + sizeof(OBJECT_NAME_INFORMATION)); + + // copy the path into the buffer + wmemcpy_s(unicodeBuffer, unicodeBufferLength, buffer.data(), buffer.size()); + + // set the null character + unicodeBuffer[buffer.size()] = L'\0'; + + // update the actual unicode string + info->Name.Buffer = unicodeBuffer; + info->Name.Length = static_cast(buffer.size() * 2); + info->Name.MaximumLength = unicodeBufferLength; + + res = STATUS_SUCCESS; + } + } + + auto logger = LOG_CALL() + .PARAMWRAP(res) + .PARAM(ObjectInformationLength) + .addParam("return_length", ReturnLength ? *ReturnLength : -1) + .addParam("tracker_path", trackerInfo) + .PARAM(ObjectInformationClass) + .PARAM(redir.redirected) + .PARAM(redir.path); + + if (res == STATUS_SUCCESS) { + logger.addParam("name_info", info->Name); + } else { + logger.addParam("name_info", ""); + } + } + + HOOK_END + return res; +} + +DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationFile( + HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, + ULONG Length, FILE_INFORMATION_CLASS FileInformationClass) +{ + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active()) { + return ::NtQueryInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, + FileInformationClass); + } + + PRE_REALCALL + res = ::NtQueryInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, + FileInformationClass); + POST_REALCALL + + // we handle both SUCCESS and BUFFER_OVERFLOW since the fixed name might be + // smaller than the original one + // + // we do not handle STATUS_INFO_LENGTH_MISMATCH because this is only returned if + // the length is too small to old the structure itself (regardless of the name) + // + // TODO: currently, this does not handle STATUS_BUFFER_OVERLOW for ALL information + // because most of the structures would need to be manually filled, which is very + // complicated - this should not pose huge issue + // + if (((res == STATUS_SUCCESS || res == STATUS_BUFFER_OVERFLOW) && + (FileInformationClass == FileNameInformation || + FileInformationClass == FileNormalizedNameInformation)) || + (res == STATUS_SUCCESS && FileInformationClass == FileAllInformation)) { + + const auto trackerInfo = ntdllHandleTracker.lookup(FileHandle); + const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo); + + // TODO: difference between FileNameInformation and FileNormalizedNameInformation + + // maximum length in bytes - the required length is + // - for ALL, 100 + the number of bytes in the name (not account for null character) + // - for NAME, 4 + the number of bytes in the name (not accounting for null + // character) + // + // it is close to the sizeof the structure + the number of bytes in the name, except + // for the alignment that gives us a bit more space + // + ULONG prefixStructLength; + FILE_NAME_INFORMATION* info; + if (FileInformationClass == FileAllInformation) { + info = &reinterpret_cast(FileInformation)->NameInformation; + prefixStructLength = sizeof(FILE_ALL_INFORMATION) - 4; + } else { + info = reinterpret_cast(FileInformation); + prefixStructLength = sizeof(FILE_NAME_INFORMATION) - 4; + } + + if (redir.redirected) { + auto requiredLength = prefixStructLength + 2 * (trackerInfo.size() - 6); + if (Length < requiredLength) { + res = STATUS_BUFFER_OVERFLOW; + } else { + // strip the \??\X: prefix (X being the drive name) + LPCWSTR filenameFixed = static_cast(trackerInfo) + 6; + + // not using SetInfoFilename because the length is not set and we do not need to + // 0-out the memory here + info->FileNameLength = static_cast((trackerInfo.size() - 6) * 2); + wmemcpy(info->FileName, filenameFixed, trackerInfo.size() - 6); + res = STATUS_SUCCESS; + + // update status block, Information is the number of bytes written, basically + // the required length + IoStatusBlock->Status = STATUS_SUCCESS; + IoStatusBlock->Information = static_cast(requiredLength); + } + } + + LOG_CALL() + .PARAMWRAP(res) + .addParam("tracker_path", trackerInfo) + .PARAM(FileInformationClass) + .PARAM(redir.redirected) + .PARAM(redir.path) + .addParam("name_info", res == STATUS_SUCCESS + ? std::wstring{info->FileName, + info->FileNameLength / sizeof(WCHAR)} + : std::wstring{}); + } + + HOOK_END + return res; +} + +unique_ptr_deleter +makeObjectAttributes(RedirectionInfo& redirInfo, POBJECT_ATTRIBUTES attributeTemplate) +{ + if (redirInfo.redirected) { + unique_ptr_deleter result(new OBJECT_ATTRIBUTES, + [](OBJECT_ATTRIBUTES* ptr) { + delete ptr; + }); + memcpy(result.get(), attributeTemplate, sizeof(OBJECT_ATTRIBUTES)); + result->RootDirectory = nullptr; + result->ObjectName = static_cast(redirInfo.path); + return result; + } else { + // just reuse the template with a dummy deleter + return unique_ptr_deleter(attributeTemplate, + [](OBJECT_ATTRIBUTES*) {}); + } +} + +DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationByName( + POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass) +{ + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + + if (!callContext.active()) { + res = ::NtQueryInformationByName(ObjectAttributes, IoStatusBlock, FileInformation, + Length, FileInformationClass); + callContext.updateLastError(); + return res; + } + + RedirectionInfo redir = + applyReroute(READ_CONTEXT(), callContext, CreateUnicodeString(ObjectAttributes)); + + if (redir.redirected) { + auto newObjectAttributes = makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtQueryInformationByName(newObjectAttributes.get(), IoStatusBlock, + FileInformation, Length, FileInformationClass); + POST_REALCALL + + LOG_CALL() + .PARAMWRAP(res) + .addParam("input_path", *ObjectAttributes->ObjectName) + .addParam("reroute_path", redir.path); + } else { + PRE_REALCALL + res = ::NtQueryInformationByName(ObjectAttributes, IoStatusBlock, FileInformation, + Length, FileInformationClass); + POST_REALCALL + } + + HOOK_END + return res; +} + +NTSTATUS ntdll_mess_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, ULONG ShareAccess, + ULONG OpenOptions) +{ + using namespace usvfs; + + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + NTSTATUS res = STATUS_NO_SUCH_FILE; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + // Why is the usual if (!callContext.active()... check missing? + + bool storePath = false; + if (((OpenOptions & FILE_DIRECTORY_FILE) != 0UL) && + ((OpenOptions & FILE_OPEN_FOR_BACKUP_INTENT) != 0UL)) { + // this may be an attempt to open a directory handle for iterating. + // If so we need to treat it a little bit differently + /* usvfs::FunctionGroupLock lock(usvfs::MutExHookGroup::FILE_ATTRIBUTES); + FILE_BASIC_INFORMATION dummy; + storePath = FAILED(NtQueryAttributesFile(ObjectAttributes, &dummy));*/ + storePath = true; + } + + UnicodeString fullName = CreateUnicodeString(ObjectAttributes); + + if (isDeviceFile(static_cast(fullName))) { + return ::NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + ShareAccess, OpenOptions); + } + + UnicodeString Path = ntdllHandleTracker.lookup(ObjectAttributes->RootDirectory); + + std::wstring checkpath = + ush::string_cast(static_cast(Path), ush::CodePage::UTF8); + + if ((fullName.size() == 0) || + (GetFileSize(ObjectAttributes->RootDirectory, nullptr) != INVALID_FILE_SIZE)) { + // //relative paths that we don't have permission over will fail here due that we + // can't get the filesize of the root directory + // //We should try again to see if it is a directory using another method + if ((fullName.size() == 0) || + (GetFileAttributesW((LPCWSTR)checkpath.c_str()) == INVALID_FILE_ATTRIBUTES)) { + return ::NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + ShareAccess, OpenOptions); + } + } + + try { + RedirectionInfo redir = applyReroute(READ_CONTEXT(), callContext, fullName); + unique_ptr_deleter adjustedAttributes = + makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtOpenFile(FileHandle, DesiredAccess, adjustedAttributes.get(), + IoStatusBlock, ShareAccess, OpenOptions); + POST_REALCALL + if (SUCCEEDED(res) && storePath) { + // store the original search path for use during iteration + READ_CONTEXT()->customData(SearchHandles)[*FileHandle] = + static_cast(fullName); +#pragma message("need to clean up this handle in CloseHandle call") + } + + if (redir.redirected) { + LOG_CALL() + .addParam("source", ObjectAttributes) + .addParam("rerouted", adjustedAttributes.get()) + .PARAM(*FileHandle) + .PARAM(OpenOptions) + .PARAMWRAP(res); + } + } catch (const std::exception&) { + PRE_REALCALL + res = ::NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + ShareAccess, OpenOptions); + POST_REALCALL + } + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG ShareAccess, ULONG OpenOptions) +{ + NTSTATUS res = ntdll_mess_NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, + IoStatusBlock, ShareAccess, OpenOptions); + if (res >= 0 && ObjectAttributes && FileHandle && + GetFileType(*FileHandle) == FILE_TYPE_DISK) + ntdllHandleTracker.insert(*FileHandle, CreateUnicodeString(ObjectAttributes)); + return res; +} + +NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + PLARGE_INTEGER AllocationSize, ULONG FileAttributes, + ULONG ShareAccess, ULONG CreateDisposition, + ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength) +{ + using namespace usvfs; + + NTSTATUS res = STATUS_NO_SUCH_FILE; + + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + if (!callContext.active()) { + return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, + CreateDisposition, CreateOptions, EaBuffer, EaLength); + } + + UnicodeString inPath = CreateUnicodeString(ObjectAttributes); + LPCWSTR inPathW = static_cast(inPath); + + if (inPath.size() == 0) { + spdlog::get("hooks")->info( + "failed to set from handle: {0}", + ush::string_cast(ObjectAttributes->ObjectName->Buffer)); + return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, + CreateDisposition, CreateOptions, EaBuffer, EaLength); + } + + if (isDeviceFile(inPathW)) { + return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, + CreateDisposition, CreateOptions, EaBuffer, EaLength); + } + + DWORD convertedDisposition = OPEN_EXISTING; + switch (CreateDisposition) { + case FILE_SUPERSEDE: + convertedDisposition = CREATE_ALWAYS; + break; + case FILE_OPEN: + convertedDisposition = OPEN_EXISTING; + break; + case FILE_CREATE: + convertedDisposition = CREATE_NEW; + break; + case FILE_OPEN_IF: + convertedDisposition = OPEN_ALWAYS; + break; + case FILE_OVERWRITE: + convertedDisposition = TRUNCATE_EXISTING; + break; + case FILE_OVERWRITE_IF: + convertedDisposition = CREATE_ALWAYS; + break; + default: + spdlog::get("hooks")->error("invalid disposition: {0}", CreateDisposition); + break; + } + + DWORD convertedAccess = 0; + if ((DesiredAccess & FILE_GENERIC_READ) == FILE_GENERIC_READ) + convertedAccess |= GENERIC_READ; + if ((DesiredAccess & FILE_GENERIC_WRITE) == FILE_GENERIC_WRITE) + convertedAccess |= GENERIC_WRITE; + if ((DesiredAccess & FILE_GENERIC_EXECUTE) == FILE_GENERIC_EXECUTE) + convertedAccess |= GENERIC_EXECUTE; + if ((DesiredAccess & FILE_ALL_ACCESS) == FILE_ALL_ACCESS) + convertedAccess |= GENERIC_ALL; + + ULONG originalDisposition = CreateDisposition; + CreateRerouter rerouter; + if (rerouter.rerouteCreate( + READ_CONTEXT(), callContext, inPathW, convertedDisposition, convertedAccess, + (LPSECURITY_ATTRIBUTES)ObjectAttributes->SecurityDescriptor)) { + switch (convertedDisposition) { + case CREATE_NEW: + CreateDisposition = FILE_CREATE; + break; + case CREATE_ALWAYS: + if (CreateDisposition != FILE_SUPERSEDE) + CreateDisposition = FILE_OVERWRITE_IF; + break; + case OPEN_EXISTING: + CreateDisposition = FILE_OPEN; + break; + case OPEN_ALWAYS: + CreateDisposition = FILE_OPEN_IF; + break; + case TRUNCATE_EXISTING: + CreateDisposition = FILE_OVERWRITE; + break; + } + + RedirectionInfo redir = applyReroute(rerouter); + + unique_ptr_deleter adjustedAttributes = + makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtCreateFile(FileHandle, DesiredAccess, adjustedAttributes.get(), + IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, + CreateDisposition, CreateOptions, EaBuffer, EaLength); + POST_REALCALL + rerouter.updateResult(callContext, res == STATUS_SUCCESS); + + if (res == STATUS_SUCCESS) { + if (rerouter.newReroute()) + rerouter.insertMapping(WRITE_CONTEXT()); + + if (rerouter.isDir() && rerouter.wasRerouted() && + ((FileAttributes & FILE_OPEN_FOR_BACKUP_INTENT) == + FILE_OPEN_FOR_BACKUP_INTENT)) { + // store the original search path for use during iteration + WRITE_CONTEXT()->customData(SearchHandles)[*FileHandle] = + inPathW; + } + } + + if (rerouter.wasRerouted() || rerouter.changedError() || + originalDisposition != CreateDisposition) { + LOG_CALL() + .PARAM(inPathW) + .PARAM(rerouter.fileName()) + .PARAMHEX(DesiredAccess) + .PARAMHEX(originalDisposition) + .PARAMHEX(CreateDisposition) + .PARAMHEX(FileAttributes) + .PARAMHEX(res) + .PARAMHEX(*FileHandle) + .PARAM(rerouter.originalError()) + .PARAM(rerouter.error()); + } + } else { + // make the original call to set up the proper errors and return statuses + PRE_REALCALL + res = ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, CreateDisposition, + CreateOptions, EaBuffer, EaLength); + POST_REALCALL + } + + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + PLARGE_INTEGER AllocationSize, + ULONG FileAttributes, ULONG ShareAccess, + ULONG CreateDisposition, ULONG CreateOptions, + PVOID EaBuffer, ULONG EaLength) +{ + NTSTATUS res = ntdll_mess_NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, + IoStatusBlock, AllocationSize, FileAttributes, + ShareAccess, CreateDisposition, CreateOptions, + EaBuffer, EaLength); + if (res >= 0 && ObjectAttributes && FileHandle && + GetFileType(*FileHandle) == FILE_TYPE_DISK) + ntdllHandleTracker.insert(*FileHandle, CreateUnicodeString(ObjectAttributes)); + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtClose(HANDLE Handle) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + NTSTATUS res = STATUS_NO_SUCH_FILE; + + HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS) + bool log = false; + + if ((::GetFileType(Handle) == FILE_TYPE_DISK)) { + HookContext::Ptr context = WRITE_CONTEXT(); + + { // clean up search data associated with this handle part 1 + Searches& activeSearches = context->customData(SearchInfo); + // std::lock_guard lock(activeSearches.queryMutex); + auto iter = activeSearches.info.find(Handle); + if (iter != activeSearches.info.end()) { + if (iter->second.currentSearchHandle != INVALID_HANDLE_VALUE) { + ::CloseHandle(iter->second.currentSearchHandle); + } + + activeSearches.info.erase(iter); + log = true; + } + } + + { + SearchHandleMap& searchHandles = + context->customData(SearchHandles); + auto iter = searchHandles.find(Handle); + if (iter != searchHandles.end()) { + searchHandles.erase(iter); + log = true; + } + } + } + + if (GetFileType(Handle) == FILE_TYPE_DISK) + ntdllHandleTracker.erase(Handle); + + PRE_REALCALL + res = ::NtClose(Handle); + POST_REALCALL + + if (log) { + LOG_CALL().PARAM(Handle).PARAMWRAP(res); + } + + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtQueryAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, PFILE_BASIC_INFORMATION FileInformation) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + // Why is the usual if (!callContext.active()... check missing? + + UnicodeString inPath = CreateUnicodeString(ObjectAttributes); + + RedirectionInfo redir = applyReroute(READ_CONTEXT(), callContext, inPath); + unique_ptr_deleter adjustedAttributes = + makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtQueryAttributesFile(adjustedAttributes.get(), FileInformation); + POST_REALCALL + + if (redir.redirected) { + LOG_CALL() + .addParam("source", ObjectAttributes) + .addParam("rerouted", adjustedAttributes.get()) + .PARAMWRAP(res); + } + + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtQueryFullAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, PFILE_NETWORK_OPEN_INFORMATION FileInformation) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + + if (!callContext.active()) { + return ::NtQueryFullAttributesFile(ObjectAttributes, FileInformation); + } + + UnicodeString inPath; + try { + inPath = CreateUnicodeString(ObjectAttributes); + } catch (const std::exception&) { + return ::NtQueryFullAttributesFile(ObjectAttributes, FileInformation); + } + + RedirectionInfo redir = applyReroute(READ_CONTEXT(), callContext, inPath); + unique_ptr_deleter adjustedAttributes = + makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtQueryFullAttributesFile(adjustedAttributes.get(), FileInformation); + POST_REALCALL + + if (redir.redirected) { + LOG_CALL() + .addParam("source", ObjectAttributes) + .addParam("rerouted", adjustedAttributes.get()) + .PARAMWRAP(res); + } + + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtTerminateProcess(HANDLE ProcessHandle, + NTSTATUS ExitStatus) +{ + // this hook is normally called when terminating another process, in which + // case there's nothing to do + // + // if the current process exits normally, the ExitProcess() hook is called + // and disconnects from the vfs; if the current process crashes, this hook + // is not called, the process just dies + // + // but a process can also terminate itself, bypassing ExitProcess(), and + // ending up here, in which case the vfs should be disconnected + // + // NtTerminateProcess() can be called two different ways to terminate the + // current process: + // - with a valid handle for the current process, or + // - with -1, because that's what GetCurrentProcess() returns + // + // it's unclear what a NULL handle represents, the behaviour is not + // documented anywhere, but looking at ReactOS, it's also interpreted as the + // current process + + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START + + const bool isCurrentProcess = ProcessHandle == (HANDLE)-1 || ProcessHandle == 0 || + GetProcessId(ProcessHandle) == GetCurrentProcessId(); + + if (isCurrentProcess) { + usvfsDisconnectVFS(); + } + + res = ::NtTerminateProcess(ProcessHandle, ExitStatus); + + HOOK_END + + return res; +} diff --git a/libs/usvfs/src/usvfs_dll/hooks/ntdll.h b/libs/usvfs/src/usvfs_dll/hooks/ntdll.h new file mode 100644 index 0000000..f3db4a6 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/ntdll.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +namespace usvfs +{ + +DLLEXPORT NTSTATUS WINAPI +hook_NtQueryFullAttributesFile(POBJECT_ATTRIBUTES ObjectAttributes, + PFILE_NETWORK_OPEN_INFORMATION FileInformation); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, PFILE_BASIC_INFORMATION FileInformation); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryDirectoryFile( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, BOOLEAN ReturnSingleEntry, + PUNICODE_STRING FileName, BOOLEAN RestartScan); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryDirectoryFileEx( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, ULONG QueryFlags, + PUNICODE_STRING FileName); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryObject( + HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass, + PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryInformationFile( + HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, + ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryInformationByName( + POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); + +DLLEXPORT NTSTATUS WINAPI hook_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG ShareAccess, ULONG OpenOptions); + +DLLEXPORT NTSTATUS WINAPI hook_NtCreateFile( + PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, + ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, + ULONG EaLength); + +DLLEXPORT NTSTATUS WINAPI hook_NtClose(HANDLE Handle); + +DLLEXPORT NTSTATUS WINAPI hook_NtTerminateProcess(HANDLE ProcessHandle, + NTSTATUS ExitStatus); + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hooks/sharedids.h b/libs/usvfs/src/usvfs_dll/hooks/sharedids.h new file mode 100644 index 0000000..9a7531c --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/sharedids.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../hookcontext.h" + +typedef std::map SearchHandleMap; + +// maps handles opened for searching to the original search path, which is +// necessary if the handle creation was rerouted +DATA_ID(SearchHandles); diff --git a/libs/usvfs/src/usvfs_dll/maptracker.h b/libs/usvfs/src/usvfs_dll/maptracker.h new file mode 100644 index 0000000..6ddf471 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/maptracker.h @@ -0,0 +1,691 @@ +#pragma once + +#include "hookcallcontext.h" +#include "hookcontext.h" +#include "stringcast.h" + +namespace usvfs +{ + +// returns true iff the path exists (checks only real paths) +static inline bool pathExists(LPCWSTR fileName) +{ + FunctionGroupLock lock(MutExHookGroup::FILE_ATTRIBUTES); + DWORD attrib = GetFileAttributesW(fileName); + return attrib != INVALID_FILE_ATTRIBUTES; +} + +// returns true iff the path exists and is a file (checks only real paths) +static inline bool pathIsFile(LPCWSTR fileName) +{ + FunctionGroupLock lock(MutExHookGroup::FILE_ATTRIBUTES); + DWORD attrib = GetFileAttributesW(fileName); + return attrib != INVALID_FILE_ATTRIBUTES && (attrib & FILE_ATTRIBUTE_DIRECTORY) == 0; +} + +// returns true iff the path exists and is a file (checks only real paths) +static inline bool pathIsDirectory(LPCWSTR fileName) +{ + FunctionGroupLock lock(MutExHookGroup::FILE_ATTRIBUTES); + DWORD attrib = GetFileAttributesW(fileName); + return attrib != INVALID_FILE_ATTRIBUTES && (attrib & FILE_ATTRIBUTE_DIRECTORY); +} + +// returns true iff the path does not exist but it parent directory does (checks only +// real paths) +static inline bool pathDirectlyAvailable(LPCWSTR pathName) +{ + FunctionGroupLock lock(MutExHookGroup::FILE_ATTRIBUTES); + DWORD attrib = GetFileAttributesW(pathName); + return attrib == INVALID_FILE_ATTRIBUTES && GetLastError() == ERROR_FILE_NOT_FOUND; +} + +class MapTracker +{ +public: + std::wstring lookup(const std::wstring& fromPath) const + { + if (!fromPath.empty()) { + std::shared_lock lock(m_mutex); + auto find = m_map.find(fromPath); + if (find != m_map.end()) + return find->second; + } + return std::wstring(); + } + + bool contains(const std::wstring& fromPath) const + { + if (!fromPath.empty()) { + std::shared_lock lock(m_mutex); + auto find = m_map.find(fromPath); + if (find != m_map.end()) + return true; + } + return false; + } + + void insert(const std::wstring& fromPath, const std::wstring& toPath) + { + if (fromPath.empty()) + return; + std::unique_lock lock(m_mutex); + m_map[fromPath] = toPath; + } + + bool erase(const std::wstring& fromPath) + { + if (fromPath.empty()) + return false; + std::unique_lock lock(m_mutex); + return m_map.erase(fromPath); + } + +private: + mutable std::shared_mutex m_mutex; + std::unordered_map m_map; +}; + +extern MapTracker k32DeleteTracker; +extern MapTracker k32FakeDirTracker; + +class RerouteW +{ + std::wstring m_Buffer{}; + std::wstring m_RealPath{}; + bool m_Rerouted{false}; + LPCWSTR m_FileName{nullptr}; + bool m_PathCreated{false}; + bool m_NewReroute{false}; + + RedirectionTree::NodePtrT m_FileNode; + +public: + RerouteW() = default; + + RerouteW(RerouteW&& reference) + : m_Buffer(std::move(reference.m_Buffer)), + m_RealPath(std::move(reference.m_RealPath)), m_Rerouted(reference.m_Rerouted), + m_PathCreated(reference.m_PathCreated), m_NewReroute(reference.m_NewReroute), + m_FileNode(std::move(reference.m_FileNode)) + { + m_FileName = reference.m_FileName != nullptr ? m_Buffer.c_str() : nullptr; + reference.m_FileName = nullptr; + } + + RerouteW& operator=(RerouteW&& reference) + { + m_Buffer = std::move(reference.m_Buffer); + m_RealPath = std::move(reference.m_RealPath); + m_Rerouted = reference.m_Rerouted; + m_PathCreated = reference.m_PathCreated; + m_NewReroute = reference.m_NewReroute; + m_FileName = reference.m_FileName != nullptr ? m_Buffer.c_str() : nullptr; + m_FileNode = std::move(reference.m_FileNode); + return *this; + } + + RerouteW(const RerouteW& reference) = delete; + RerouteW& operator=(const RerouteW&) = delete; + + LPCWSTR fileName() const { return m_FileName; } + + const std::wstring& buffer() const { return m_Buffer; } + + bool wasRerouted() const { return m_Rerouted; } + + bool newReroute() const { return m_NewReroute; } + + void insertMapping(const HookContext::Ptr& context, bool directory = false) + { + if (directory) { + addDirectoryMapping(context, m_RealPath, m_FileName); + + // In case we have just created a "fake" directory, it is no longer fake and need + // to remove it and all its parent folders from the fake map: + std::wstring dir = m_FileName; + while (k32FakeDirTracker.erase(dir)) + dir = fs::path(dir).parent_path().wstring(); + } else { + // if (m_PathCreated) + // addDirectoryMapping(context, fs::path(m_RealPath).parent_path(), + // fs::path(m_FileName).parent_path()); + + spdlog::get("hooks")->info( + "mapping file in vfs: {}, {}", + shared::string_cast(m_RealPath, shared::CodePage::UTF8), + shared::string_cast(m_FileName, shared::CodePage::UTF8)); + m_FileNode = context->redirectionTable().addFile( + m_RealPath, RedirectionDataLocal(shared::string_cast( + m_FileName, shared::CodePage::UTF8))); + + k32DeleteTracker.erase(m_RealPath); + } + } + + void removeMapping(const HookContext::ConstPtr& readContext, bool directory = false) + { + bool addToDelete = false; + bool dontAddToDelete = false; + + // We need to track deleted files even if they were not rerouted (i.e. files deleted + // from the real folder which there is a virtualized mapped folder on top of it). + // Since we don't want to add, *every* file which is deleted we check this: + bool found = wasRerouted(); + if (!found) { + FindCreateTarget visitor; + RedirectionTree::VisitorFunction visitorWrapper = + [&](const RedirectionTree::NodePtrT& node) { + visitor(node); + }; + readContext->redirectionTable()->visitPath(m_RealPath, visitorWrapper); + if (visitor.target.get()) + found = true; + } + if (found) + addToDelete = true; + + if (wasRerouted()) { + if (m_FileNode.get()) + m_FileNode->removeFromTree(); + else + spdlog::get("usvfs")->warn("Node not removed: {}", + shared::string_cast(m_FileName)); + + if (!directory) { + // check if this file was the last file inside a "fake" directory then remove it + // and possibly also its fake empty parent folders: + std::wstring parent = m_FileName; + while (true) { + parent = fs::path(parent).parent_path().wstring(); + if (k32FakeDirTracker.contains(parent)) { + dontAddToDelete = true; + if (RemoveDirectoryW(parent.c_str())) { + k32FakeDirTracker.erase(parent); + spdlog::get("usvfs")->info("removed empty fake directory: {}", + shared::string_cast(parent)); + } else if (GetLastError() != ERROR_DIR_NOT_EMPTY) { + auto error = GetLastError(); + spdlog::get("usvfs")->warn("removing fake directory failed: {}, error={}", + shared::string_cast(parent), + error); + break; + } + } else + break; + } + } + } + if (addToDelete && !dontAddToDelete) { + k32DeleteTracker.insert(m_RealPath, m_FileName); + } + } + + static bool createFakePath(fs::path path, LPSECURITY_ATTRIBUTES securityAttributes) + { + // sanity and guaranteed recursion end: + if (!path.has_relative_path()) + throw shared::windows_error( + "createFakePath() refusing to create non-existing top level path: " + + path.string()); + + DWORD attr = GetFileAttributesW(path.c_str()); + DWORD err = GetLastError(); + if (attr != INVALID_FILE_ATTRIBUTES) { + if (attr & FILE_ATTRIBUTE_DIRECTORY) + return false; // if directory already exists all is good + else + throw shared::windows_error("createFakePath() called on a file: " + + path.string()); + } + if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND) + throw shared::windows_error( + "createFakePath() GetFileAttributesW failed on: " + path.string(), err); + + if (err != ERROR_FILE_NOT_FOUND) // ERROR_FILE_NOT_FOUND means parent directory + // already exists + createFakePath( + path.parent_path(), + securityAttributes); // otherwise create parent directory (recursively) + + BOOL res = CreateDirectoryW(path.c_str(), securityAttributes); + if (res) + k32FakeDirTracker.insert(path.wstring(), std::wstring()); + else { + err = GetLastError(); + throw shared::windows_error( + "createFakePath() CreateDirectoryW failed on: " + path.string(), err); + } + return true; + } + + static bool addDirectoryMapping(const HookContext::Ptr& context, + const fs::path& originalPath, + const fs::path& reroutedPath) + { + if (originalPath.empty() || reroutedPath.empty()) { + spdlog::get("hooks")->error("RerouteW::addDirectoryMapping failed: {}, {}", + shared::string_cast( + originalPath.wstring(), shared::CodePage::UTF8) + .c_str(), + shared::string_cast( + reroutedPath.wstring(), shared::CodePage::UTF8) + .c_str()); + return false; + } + + auto lookupParent = + context->redirectionTable()->findNode(originalPath.parent_path()); + if (!lookupParent.get() || lookupParent->data().linkTarget.empty()) { + if (!addDirectoryMapping(context, originalPath.parent_path(), + reroutedPath.parent_path())) { + spdlog::get("hooks")->error("RerouteW::addDirectoryMapping failed: {}, {}", + shared::string_cast( + originalPath.wstring(), shared::CodePage::UTF8) + .c_str(), + shared::string_cast( + reroutedPath.wstring(), shared::CodePage::UTF8) + .c_str()); + return false; + } + } + + std::string reroutedU8 = shared::string_cast(reroutedPath.wstring(), + shared::CodePage::UTF8); + if (reroutedU8.empty() || reroutedU8[reroutedU8.size() - 1] != '\\') + reroutedU8 += "\\"; + + spdlog::get("hooks")->info("mapping directory in vfs: {}, {}", + shared::string_cast(originalPath.wstring(), + shared::CodePage::UTF8), + reroutedU8.c_str()); + + context->redirectionTable().addDirectory( + originalPath, RedirectionDataLocal(reroutedU8), + shared::FLAG_DIRECTORY | shared::FLAG_CREATETARGET); + + fs::directory_iterator end_itr; + + // cycle through the directory + for (fs::directory_iterator itr(reroutedPath); itr != end_itr; ++itr) { + // If it's not a directory, add it to the VFS, if it is recurse into it + if (is_regular_file(itr->path())) { + std::string fileReroutedU8 = shared::string_cast( + itr->path().wstring(), shared::CodePage::UTF8); + spdlog::get("hooks")->info( + "mapping file in vfs: {}, {}", + shared::string_cast( + (originalPath / itr->path().filename()).wstring(), + shared::CodePage::UTF8), + fileReroutedU8.c_str()); + context->redirectionTable().addFile( + fs::path(originalPath / itr->path().filename()), + RedirectionDataLocal(fileReroutedU8)); + } else { + addDirectoryMapping(context, originalPath / itr->path().filename(), + reroutedPath / itr->path().filename()); + } + } + + return true; + } + + template + static bool interestingPathImpl(const char_t* inPath) + { + if (!inPath || !inPath[0]) + return false; + // ignore \\.\ unless its a \\.\?: + if (inPath[0] == '\\' && inPath[1] == '\\' && inPath[2] == '.' && + inPath[3] == '\\' && (!inPath[4] || inPath[5] != ':')) + return false; + // ignore L"hid#": + if ((inPath[0] == 'h' || inPath[0] == 'H') && + ((inPath[1] == 'i' || inPath[1] == 'I')) && + ((inPath[2] == 'd' || inPath[2] == 'D')) && inPath[3] == '#') + return false; + return true; + } + + static bool interestingPath(const char* inPath) + { + return interestingPathImpl(inPath); + } + static bool interestingPath(const wchar_t* inPath) + { + return interestingPathImpl(inPath); + } + + static fs::path absolutePath(const wchar_t* inPath) + { + if (shared::startswith(inPath, LR"(\\?\)") || + shared::startswith(inPath, LR"(\??\)")) { + inPath += 4; + return inPath; + } else if ((shared::startswith(inPath, LR"(\\localhost\)") || + shared::startswith(inPath, LR"(\\127.0.0.1\)")) && + inPath[13] == L'$') { + std::wstring newPath; + newPath += towupper(inPath[12]); + newPath += L':'; + newPath += &inPath[14]; + return newPath; + } else if (inPath[0] == L'\0' || inPath[1] == L':') { + return inPath; + } else if (inPath[0] == L'\\' || inPath[0] == L'/') { + return fs::path(winapi::wide::getFullPathName(inPath).first); + } + WCHAR currentDirectory[MAX_PATH]; + ::GetCurrentDirectoryW(MAX_PATH, currentDirectory); + fs::path finalPath = fs::path(currentDirectory) / inPath; + return finalPath; + // winapi::wide::getFullPathName(inPath).first; + } + + static fs::path canonizePath(const fs::path& inPath) + { + fs::path p = inPath.lexically_normal(); + if (p.filename_is_dot()) + p = p.remove_filename(); + return p.make_preferred(); + } + + static RerouteW create(const HookContext::ConstPtr& context, + const HookCallContext& callContext, const wchar_t* inPath, + bool inverse = false) + { + RerouteW result; + + if (interestingPath(inPath) && callContext.active()) { + const auto& lookupPath = canonizePath(absolutePath(inPath)); + result.m_RealPath = lookupPath.wstring(); + + result.m_Buffer = k32DeleteTracker.lookup(result.m_RealPath); + bool found = !result.m_Buffer.empty(); + if (found) { + spdlog::get("hooks")->info( + "Rerouting file open to location of deleted file: {}", + shared::string_cast(result.m_Buffer)); + result.m_NewReroute = true; + } else { + const RedirectionTreeContainer& table = + inverse ? context->inverseTable() : context->redirectionTable(); + result.m_FileNode = table->findNode(lookupPath); + + if (result.m_FileNode.get() && (!result.m_FileNode->data().linkTarget.empty() || + result.m_FileNode->isDirectory())) { + if (!result.m_FileNode->data().linkTarget.empty()) { + result.m_Buffer = shared::string_cast( + result.m_FileNode->data().linkTarget.c_str(), shared::CodePage::UTF8); + } else { + result.m_Buffer = result.m_FileNode->path().wstring(); + } + found = true; + } + } + if (found) { + result.m_Rerouted = true; + + wchar_t inIt = inPath[wcslen(inPath) - 1]; + std::wstring::iterator outIt = result.m_Buffer.end() - 1; + if ((*outIt == L'\\' || *outIt == L'/') && !(inIt == L'\\' || inIt == L'/')) + result.m_Buffer.erase(outIt); + std::replace(result.m_Buffer.begin(), result.m_Buffer.end(), L'/', L'\\'); + } else + result.m_Buffer = inPath; + } else if (inPath) + result.m_Buffer = inPath; + + if (inPath) + result.m_FileName = result.m_Buffer.c_str(); + return result; + } + + static RerouteW createNew(const HookContext::ConstPtr& context, + const HookCallContext& callContext, LPCWSTR inPath, + bool createPath = true, + LPSECURITY_ATTRIBUTES securityAttributes = nullptr) + { + RerouteW result; + + if (interestingPath(inPath) && callContext.active()) { + const auto& lookupPath = canonizePath(absolutePath(inPath)); + result.m_RealPath = lookupPath.wstring(); + + result.m_Buffer = k32DeleteTracker.lookup(result.m_RealPath); + bool found = !result.m_Buffer.empty(); + if (found) + spdlog::get("hooks")->info( + "Rerouting file creation to original location of deleted file: {}", + shared::string_cast(result.m_Buffer)); + else { + FindCreateTarget visitor; + RedirectionTree::VisitorFunction visitorWrapper = + [&](const RedirectionTree::NodePtrT& node) { + visitor(node); + }; + context->redirectionTable()->visitPath(lookupPath, visitorWrapper); + if (visitor.target.get()) { + // the visitor has found the last (deepest in the directory hierarchy) + // create-target + fs::path relativePath = + shared::make_relative(visitor.target->path(), lookupPath); + result.m_Buffer = + (fs::path(visitor.target->data().linkTarget.c_str()) / relativePath) + .wstring(); + found = true; + } + } + + if (found) { + if (createPath) { + try { + FunctionGroupLock lock(MutExHookGroup::ALL_GROUPS); + result.m_PathCreated = createFakePath( + fs::path(result.m_Buffer).parent_path(), securityAttributes); + } catch (const std::exception& e) { + spdlog::get("hooks")->error( + "failed to create {}: {}", + shared::string_cast(result.m_Buffer), e.what()); + } + } + + wchar_t inIt = inPath[wcslen(inPath) - 1]; + std::wstring::iterator outIt = result.m_Buffer.end() - 1; + if ((*outIt == L'\\' || *outIt == L'/') && !(inIt == L'\\' || inIt == L'/')) + result.m_Buffer.erase(outIt); + std::replace(result.m_Buffer.begin(), result.m_Buffer.end(), L'/', L'\\'); + result.m_Rerouted = true; + result.m_NewReroute = true; + } else + result.m_Buffer = inPath; + } else if (inPath) + result.m_Buffer = inPath; + + if (inPath) + result.m_FileName = result.m_Buffer.c_str(); + return result; + } + + static RerouteW createOrNew(const HookContext::ConstPtr& context, + const HookCallContext& callContext, LPCWSTR inPath, + bool createPath = true, + LPSECURITY_ATTRIBUTES securityAttributes = nullptr) + { + { + auto res = create(context, callContext, inPath); + if (res.wasRerouted() || !interestingPath(inPath) || !callContext.active() || + pathExists(inPath)) + return std::move(res); + } + return createNew(context, callContext, inPath, createPath, securityAttributes); + } + + static RerouteW noReroute(LPCWSTR inPath) + { + RerouteW result; + if (inPath) + result.m_Buffer = inPath; + if (inPath && inPath[0] && !shared::startswith(inPath, L"hid#")) + std::replace(result.m_Buffer.begin(), result.m_Buffer.end(), L'/', L'\\'); + result.m_FileName = result.m_Buffer.c_str(); + return result; + } + +private: + struct FindCreateTarget + { + RedirectionTree::NodePtrT target; + void operator()(RedirectionTree::NodePtrT node) + { + if (node->hasFlag(shared::FLAG_CREATETARGET)) { + target = node; + } + } + }; +}; + +class CreateRerouter +{ +public: + bool rerouteCreate(const HookContext::ConstPtr& context, + const HookCallContext& callContext, LPCWSTR lpFileName, + DWORD& dwCreationDisposition, DWORD dwDesiredAccess, + LPSECURITY_ATTRIBUTES lpSecurityAttributes) + { + enum class Open + { + existing, + create, + empty + }; + Open open = Open::existing; + + // Notice since we are calling our patched GetFileAttributesW here this will also + // check virtualized paths + DWORD virtAttr = GetFileAttributesW(lpFileName); + bool isFile = virtAttr != INVALID_FILE_ATTRIBUTES && + (virtAttr & FILE_ATTRIBUTE_DIRECTORY) == 0; + m_isDir = + virtAttr != INVALID_FILE_ATTRIBUTES && (virtAttr & FILE_ATTRIBUTE_DIRECTORY); + + switch (dwCreationDisposition) { + case CREATE_ALWAYS: + open = Open::create; + if (isFile || m_isDir) { + m_error = ERROR_ALREADY_EXISTS; + } + break; + + case CREATE_NEW: + if (isFile || m_isDir) { + m_error = ERROR_FILE_EXISTS; + return false; + } else { + open = Open::create; + } + break; + + case OPEN_ALWAYS: + if (isFile || m_isDir) { + m_error = ERROR_ALREADY_EXISTS; + } else { + open = Open::create; + } + break; + + case TRUNCATE_EXISTING: + if ((dwDesiredAccess & GENERIC_WRITE) == 0) { + m_error = ERROR_INVALID_PARAMETER; + return false; + } + if (isFile || m_isDir) + open = Open::empty; + break; + } + + if (m_isDir && pathIsDirectory(lpFileName)) + m_reroute = RerouteW::noReroute(lpFileName); + else + m_reroute = RerouteW::create(context, callContext, lpFileName); + + if (m_reroute.wasRerouted() && open == Open::create && + pathIsDirectory(m_reroute.fileName())) + m_reroute = RerouteW::createNew(context, callContext, lpFileName, true, + lpSecurityAttributes); + + if (!m_isDir && !isFile && !m_reroute.wasRerouted() && + (open == Open::create || open == Open::empty)) { + m_reroute = RerouteW::createNew(context, callContext, lpFileName, true, + lpSecurityAttributes); + + bool newFile = + !m_reroute.wasRerouted() && pathDirectlyAvailable(m_reroute.fileName()); + if (newFile && open == Open::empty) + // TRUNCATE_EXISTING will fail since the new file doesn't exist, so change + // disposition: + dwCreationDisposition = CREATE_ALWAYS; + } + + return true; + } + + // rerouteNew is used for rerouting the destination of copy/move operations. Assumes + // that the call will be skipped if false is returned. + bool rerouteNew(const HookContext::ConstPtr& context, HookCallContext& callContext, + LPCWSTR lpFileName, bool replaceExisting, const char* hookName) + { + DWORD disposition = replaceExisting ? CREATE_ALWAYS : CREATE_NEW; + if (!rerouteCreate(context, callContext, lpFileName, disposition, GENERIC_WRITE, + nullptr)) { + spdlog::get("hooks")->info( + "{} guaranteed failure, skipping original call: {}, replaceExisting={}, " + "error={}", + hookName, + shared::string_cast(lpFileName, shared::CodePage::UTF8), + replaceExisting ? "true" : "false", error()); + + callContext.updateLastError(error()); + return false; + } + return true; + } + + void updateResult(HookCallContext& callContext, bool success) + { + m_originalError = callContext.lastError(); + if (success) { + // m_error != ERROR_SUCCESS means we are overriding the error on success + if (m_error == ERROR_SUCCESS) + m_error = m_originalError; + } else if (m_originalError == ERROR_PATH_NOT_FOUND && m_directlyAvailable) + m_error = ERROR_FILE_NOT_FOUND; + else + m_error = m_originalError; + if (m_error != m_originalError) + callContext.updateLastError(m_error); + } + + DWORD error() const { return m_error; } + DWORD originalError() const { return m_originalError; } + bool changedError() const { return m_error != m_originalError; } + + bool isDir() const { return m_isDir; } + bool newReroute() const { return m_reroute.newReroute(); } + bool wasRerouted() const { return m_reroute.wasRerouted(); } + LPCWSTR fileName() const { return m_reroute.fileName(); } + + void insertMapping(const HookContext::Ptr& context, bool directory = false) + { + m_reroute.insertMapping(context, directory); + } + +private: + DWORD m_error = ERROR_SUCCESS; + DWORD m_originalError = ERROR_SUCCESS; + bool m_directlyAvailable = false; + bool m_isDir = false; + RerouteW m_reroute; +}; + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/pch.cpp b/libs/usvfs/src/usvfs_dll/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/libs/usvfs/src/usvfs_dll/redirectiontree.cpp b/libs/usvfs/src/usvfs_dll/redirectiontree.cpp new file mode 100644 index 0000000..e521365 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/redirectiontree.cpp @@ -0,0 +1,27 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "redirectiontree.h" + +std::ostream& usvfs::operator<<(std::ostream& stream, const RedirectionData& data) +{ + stream << data.linkTarget; + return stream; +} diff --git a/libs/usvfs/src/usvfs_dll/redirectiontree.h b/libs/usvfs/src/usvfs_dll/redirectiontree.h new file mode 100644 index 0000000..6a2238a --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/redirectiontree.h @@ -0,0 +1,104 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include + +namespace usvfs +{ + +// typedef boost::interprocess::basic_string, +// shared::CharAllocatorT> StringT; + +namespace shared +{ + static const TreeFlags FLAG_CREATETARGET = FLAG_FIRSTUSERFLAG + 0x00; +} + +struct RedirectionDataLocal +{ + + RedirectionDataLocal(const char* target) : linkTarget(target) {} + + RedirectionDataLocal(const std::string& target) : linkTarget(target) {} + + std::string linkTarget; +}; + +struct RedirectionData +{ + + RedirectionData(const RedirectionData& reference, + const shared::VoidAllocatorT& allocator) + : linkTarget(reference.linkTarget.c_str(), allocator) + {} + + RedirectionData(const RedirectionDataLocal& reference, + const shared::VoidAllocatorT& allocator) + : linkTarget(reference.linkTarget.c_str(), allocator) + {} + + RedirectionData(const char* target, const shared::VoidAllocatorT& allocator) + : linkTarget(target, allocator) + {} + + shared::StringT linkTarget; +}; + +std::ostream& operator<<(std::ostream& stream, const RedirectionData& data); + +template <> +inline void shared::dataAssign(RedirectionData& destination, + const RedirectionData& source) +{ + destination.linkTarget.assign(source.linkTarget.c_str()); +} + +template <> +inline RedirectionData +shared::createDataEmpty(const VoidAllocatorT& allocator) +{ + return RedirectionData("", allocator); +} + +template +struct shared::SHMDataCreator +{ + static RedirectionData create(T source, const VoidAllocatorT& allocator) + { + return RedirectionData(source, allocator); + } +}; + +template <> +struct shared::SHMDataCreator +{ + static RedirectionData create(const RedirectionData& source, + const VoidAllocatorT& allocator) + { + return RedirectionData(source, allocator); + } +}; + +using RedirectionTree = shared::DirectoryTree; +using RedirectionTreeContainer = shared::TreeContainer; + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/semaphore.cpp b/libs/usvfs/src/usvfs_dll/semaphore.cpp new file mode 100644 index 0000000..609b2f5 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/semaphore.cpp @@ -0,0 +1,57 @@ +#include "semaphore.h" +#include "exceptionex.h" + +RecursiveBenaphore::RecursiveBenaphore() : m_Counter(0), m_OwnerId(0UL), m_Recursion(0) +{ + m_Semaphore = ::CreateSemaphore(nullptr, 1, 1, nullptr); +} + +RecursiveBenaphore::~RecursiveBenaphore() +{ + ::CloseHandle(m_Semaphore); +} + +void RecursiveBenaphore::wait(DWORD timeout) +{ + DWORD tid = ::GetCurrentThreadId(); + + if (::_InterlockedIncrement(&m_Counter) > 1) { + if (tid != m_OwnerId) { + int tries = 3; + while (::WaitForSingleObject(m_Semaphore, timeout) != WAIT_OBJECT_0) { + HANDLE owner = ::OpenThread(SYNCHRONIZE, FALSE, m_OwnerId); + ON_BLOCK_EXIT([owner]() { + ::CloseHandle(owner); + }); + if ((tries <= 0) || (::WaitForSingleObject(owner, 0) == WAIT_OBJECT_0)) { + // owner has quit without releasing the semaphore! + m_Recursion = 0; + spdlog::get("usvfs")->error("thread {} never released the mutex", m_OwnerId); + break; + } else { + --tries; + } + } + } + } + m_OwnerId = tid; + ++m_Recursion; +} + +void RecursiveBenaphore::signal() +{ + if (m_Recursion == 0) { + return; + } + // no validation the signaling thread is the one owning the lock + DWORD recursion = --m_Recursion; + if (recursion == 0) { + m_OwnerId = 0; + } + DWORD result = ::_InterlockedDecrement(&m_Counter); + if (result > 0) { + if (recursion == 0) { + ::ReleaseSemaphore(m_Semaphore, 1, nullptr); + } + } +} diff --git a/libs/usvfs/src/usvfs_dll/semaphore.h b/libs/usvfs/src/usvfs_dll/semaphore.h new file mode 100644 index 0000000..1048c47 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/semaphore.h @@ -0,0 +1,27 @@ +#pragma once + +// based on code by Jeff Preshing + +// this is a synchronization class that prefers +// undefined behaviour over deadlock. It's utterly broken +// and needs to be replaced in time. + +class RecursiveBenaphore +{ + +public: + RecursiveBenaphore(); + ~RecursiveBenaphore(); + + // wait on the semaphore. after timeout this will check if the current owner + // thread is still alive and steal the semaphore if it isn't. Otherwise this + // will continue to wait. + void wait(DWORD timeout = INFINITE); + void signal(); + +private: + LONG m_Counter; + DWORD m_OwnerId; + int m_Recursion; + HANDLE m_Semaphore; +}; diff --git a/libs/usvfs/src/usvfs_dll/sharedparameters.cpp b/libs/usvfs/src/usvfs_dll/sharedparameters.cpp new file mode 100644 index 0000000..95cd0b2 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/sharedparameters.cpp @@ -0,0 +1,264 @@ +#include +#include +#include + +namespace usvfs +{ + +ForcedLibrary::ForcedLibrary(const std::string& process, const std::string& path, + const shared::VoidAllocatorT& alloc) + : m_processName(process.begin(), process.end(), alloc), + m_libraryPath(path.begin(), path.end(), alloc) +{} + +std::string ForcedLibrary::processName() const +{ + return {m_processName.begin(), m_processName.end()}; +} + +std::string ForcedLibrary::libraryPath() const +{ + return {m_libraryPath.begin(), m_libraryPath.end()}; +} + +SharedParameters::SharedParameters(const usvfsParameters& reference, + const shared::VoidAllocatorT& allocator) + : m_instanceName(reference.instanceName, allocator), + m_currentSHMName(reference.currentSHMName, allocator), + m_currentInverseSHMName(reference.currentInverseSHMName, allocator), + m_debugMode(reference.debugMode), m_logLevel(reference.logLevel), + m_crashDumpsType(reference.crashDumpsType), + m_crashDumpsPath(reference.crashDumpsPath, allocator), + m_delayProcess(reference.delayProcessMs), m_userCount(1), + m_processBlacklist(allocator), m_processList(allocator), + m_fileSuffixSkipList(allocator), m_directorySkipList(allocator), + m_forcedLibraries(allocator) +{} + +usvfsParameters SharedParameters::makeLocal() const +{ + bi::scoped_lock lock(m_mutex); + + return usvfsParameters(m_instanceName.c_str(), m_currentSHMName.c_str(), + m_currentInverseSHMName.c_str(), m_debugMode, m_logLevel, + m_crashDumpsType, m_crashDumpsPath.c_str(), + m_delayProcess.count()); +} + +std::string SharedParameters::instanceName() const +{ + bi::scoped_lock lock(m_mutex); + return {m_instanceName.begin(), m_instanceName.end()}; +} + +std::string SharedParameters::currentSHMName() const +{ + bi::scoped_lock lock(m_mutex); + return {m_currentSHMName.begin(), m_currentSHMName.end()}; +} + +std::string SharedParameters::currentInverseSHMName() const +{ + bi::scoped_lock lock(m_mutex); + return {m_currentInverseSHMName.begin(), m_currentInverseSHMName.end()}; +} + +void SharedParameters::setSHMNames(const std::string& current, + const std::string& inverse) +{ + bi::scoped_lock lock(m_mutex); + + m_currentSHMName.assign(current.begin(), current.end()); + m_currentInverseSHMName.assign(inverse.begin(), inverse.end()); +} + +void SharedParameters::setDebugParameters(LogLevel level, CrashDumpsType dumpType, + const std::string& dumpPath, + std::chrono::milliseconds delayProcess) +{ + bi::scoped_lock lock(m_mutex); + + m_logLevel = level; + m_crashDumpsType = dumpType; + m_crashDumpsPath.assign(dumpPath.begin(), dumpPath.end()); + m_delayProcess = delayProcess; +} + +std::size_t SharedParameters::userConnected() +{ + bi::scoped_lock lock(m_mutex); + return ++m_userCount; +} + +std::size_t SharedParameters::userDisconnected() +{ + bi::scoped_lock lock(m_mutex); + return --m_userCount; +} + +std::size_t SharedParameters::userCount() +{ + bi::scoped_lock lock(m_mutex); + return m_userCount; +} + +std::size_t SharedParameters::registeredProcessCount() const +{ + bi::scoped_lock lock(m_mutex); + return m_processList.size(); +} + +std::vector SharedParameters::registeredProcesses() const +{ + bi::scoped_lock lock(m_mutex); + return {m_processList.begin(), m_processList.end()}; +} + +void SharedParameters::registerProcess(DWORD pid) +{ + bi::scoped_lock lock(m_mutex); + m_processList.insert(pid); +} + +void SharedParameters::unregisterProcess(DWORD pid) +{ + { + bi::scoped_lock lock(m_mutex); + + auto itor = m_processList.find(pid); + + if (itor != m_processList.end()) { + m_processList.erase(itor); + return; + } + } + + spdlog::get("usvfs")->error("cannot unregister process {}, not in list", pid); +} + +void SharedParameters::blacklistExecutable(const std::string& name) +{ + bi::scoped_lock lock(m_mutex); + + m_processBlacklist.insert( + shared::StringT(name.begin(), name.end(), m_processBlacklist.get_allocator())); +} + +void SharedParameters::clearExecutableBlacklist() +{ + bi::scoped_lock lock(m_mutex); + m_processBlacklist.clear(); +} + +bool SharedParameters::executableBlacklisted(const std::string& appName, + const std::string& cmdLine) const +{ + bool blacklisted = false; + std::string log; + + { + bi::scoped_lock lock(m_mutex); + + for (const shared::StringT& sitem : m_processBlacklist) { + const auto item = "\\" + std::string(sitem.begin(), sitem.end()); + + if (!appName.empty()) { + if (boost::algorithm::iends_with(appName, item)) { + blacklisted = true; + log = std::format("application {} is blacklisted", appName); + break; + } + } + + if (!cmdLine.empty()) { + if (boost::algorithm::icontains(cmdLine, item)) { + blacklisted = true; + log = std::format("command line {} is blacklisted", cmdLine); + break; + } + } + } + } + + if (blacklisted) { + spdlog::get("usvfs")->info(log); + return true; + } + + return false; +} + +void SharedParameters::addSkipFileSuffix(const std::string& fileSuffix) +{ + bi::scoped_lock lock(m_mutex); + + m_fileSuffixSkipList.insert(shared::StringT(fileSuffix.begin(), fileSuffix.end(), + m_fileSuffixSkipList.get_allocator())); +} + +void SharedParameters::clearSkipFileSuffixes() +{ + bi::scoped_lock lock(m_mutex); + m_fileSuffixSkipList.clear(); +} + +std::vector SharedParameters::skipFileSuffixes() const +{ + bi::scoped_lock lock(m_mutex); + return {m_fileSuffixSkipList.begin(), m_fileSuffixSkipList.end()}; +} + +void SharedParameters::addSkipDirectory(const std::string& directory) +{ + bi::scoped_lock lock(m_mutex); + + m_directorySkipList.insert(shared::StringT(directory.begin(), directory.end(), + m_directorySkipList.get_allocator())); +} + +void SharedParameters::clearSkipDirectories() +{ + bi::scoped_lock lock(m_mutex); + m_directorySkipList.clear(); +} + +std::vector SharedParameters::skipDirectories() const +{ + bi::scoped_lock lock(m_mutex); + return {m_directorySkipList.begin(), m_directorySkipList.end()}; +} + +void SharedParameters::addForcedLibrary(const std::string& processName, + const std::string& libraryPath) +{ + bi::scoped_lock lock(m_mutex); + + m_forcedLibraries.push_front( + ForcedLibrary(processName, libraryPath, m_forcedLibraries.get_allocator())); +} + +std::vector +SharedParameters::forcedLibraries(const std::string& processName) +{ + std::vector v; + + { + bi::scoped_lock lock(m_mutex); + + for (const auto& lib : m_forcedLibraries) { + if (boost::algorithm::iequals(processName, lib.processName())) { + v.push_back(lib.libraryPath()); + } + } + } + + return v; +} + +void SharedParameters::clearForcedLibraries() +{ + bi::scoped_lock lock(m_mutex); + m_forcedLibraries.clear(); +} + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/stringcast_boost.h b/libs/usvfs/src/usvfs_dll/stringcast_boost.h new file mode 100644 index 0000000..d2544ec --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/stringcast_boost.h @@ -0,0 +1,42 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include + +namespace usvfs +{ +namespace shared +{ + + template + class string_cast_impl> + { + public: + static ToT + cast(const boost::container::basic_string& source, + CodePage codePage, size_t sourceLength) + { + return string_cast_impl::cast(source.c_str(), codePage, + sourceLength); + } + }; + +} // namespace shared +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/usvfs.cpp b/libs/usvfs/src/usvfs_dll/usvfs.cpp new file mode 100644 index 0000000..56d9f71 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/usvfs.cpp @@ -0,0 +1,965 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include "usvfs.h" +#include "hookmanager.h" +#include "loghelpers.h" +#include "redirectiontree.h" +#include "usvfs_version.h" +#include "usvfsparametersprivate.h" +#include +#include +#include +#include +#include +#include +#include + +// note that there's a mix of boost and std filesystem stuff in this file and +// that they're not completely compatible +#include + +namespace bfs = boost::filesystem; +namespace ush = usvfs::shared; +namespace bip = boost::interprocess; +namespace ba = boost::algorithm; + +using usvfs::log::ConvertLogLevel; + +usvfs::HookManager* manager = nullptr; +usvfs::HookContext* context = nullptr; +HMODULE dllModule = nullptr; +PVOID exceptionHandler = nullptr; +CrashDumpsType usvfs_dump_type = CrashDumpsType::None; +std::wstring usvfs_dump_path; + +// this is called for every single file, so it's a bit long winded, but it's +// as fast as it gets, probably +// +template +bool extensionMatchesCI( + std::string_view name, + const std::array& extensionsLC, + const std::array& extensionsUC) +{ + constexpr std::size_t longestExtensionWithDot = LongestExtension + 1; + + // quick check + if (name.size() < longestExtensionWithDot) { + return false; + } + + // for each extension + for (std::size_t i = 0; i < ExtensionsCount; ++i) { + const std::size_t extensionLength = extensionsLC[i].size(); + const std::size_t extensionLengthWithDot = extensionLength + 1; + + // check size + if (name.size() < extensionLengthWithDot) { + continue; + } + + // check dot + if (name[name.size() - extensionLengthWithDot] != '.') { + continue; + } + + // starts at one past the dot + const auto* p = name.data() + name.size() - extensionLength; + + // set to false as soon as a character doesn't match + bool found = true; + + // for each character in extension + for (std::size_t c = 0; c < extensionLength; ++c) { + // checking both lowercase and uppercase + if (*p != extensionsLC[i][c] && *p != extensionsUC[i][c]) { + // neither + found = false; + break; + } + + // matches, check next + ++p; + } + + if (found) { + return true; + } + } + + return false; +} + +bool shouldAddToInverseTree(std::string_view name) +{ + static std::array extensionsLC{"exe", "dll"}; + static std::array extensionsUC{"EXE", "DLL"}; + + // must be changed if any extension longer than 3 letters is added + constexpr std::size_t longestExtension = 3; + + return extensionMatchesCI(name, extensionsLC, extensionsUC); +} + +// +// Logging +// + +void InitLoggingInternal(bool toConsole, bool connectExistingSHM) +{ + try { + if (!toConsole && !SHMLogger::isInstantiated()) { + if (connectExistingSHM) { + SHMLogger::open("usvfs"); + } else { + SHMLogger::create("usvfs"); + } + } + + // a temporary logger was created in DllMain + spdlog::drop("usvfs"); +#pragma message("need a customized name for the shm") + auto logger = spdlog::get("usvfs"); + if (logger.get() == nullptr) { + logger = toConsole ? spdlog::create("usvfs") + : spdlog::create("usvfs", "usvfs"); + logger->set_pattern("%H:%M:%S.%e [%L] %v"); + } + logger->set_level(spdlog::level::debug); + + spdlog::drop("hooks"); + logger = spdlog::get("hooks"); + if (logger.get() == nullptr) { + logger = toConsole ? spdlog::create("hooks") + : spdlog::create("hooks", "usvfs"); + logger->set_pattern("%H:%M:%S.%e <%P:%t> [%L] %v"); + } + logger->set_level(spdlog::level::debug); + } catch (const std::exception&) { + // TODO should really report this + // OutputDebugStringA((boost::format("init exception: %1%\n") % + // e.what()).str().c_str()); + if (spdlog::get("usvfs").get() == nullptr) { + spdlog::create("usvfs"); + } + if (spdlog::get("hooks").get() == nullptr) { + spdlog::create("hooks"); + } + } + + spdlog::get("usvfs")->info("usvfs dll {} initialized in process {}", + USVFS_VERSION_STRING, GetCurrentProcessId()); +} + +void WINAPI usvfsInitLogging(bool toConsole) +{ + InitLoggingInternal(toConsole, false); +} + +extern "C" DLLEXPORT bool WINAPI usvfsGetLogMessages(LPSTR buffer, size_t size, + bool blocking) +{ + buffer[0] = '\0'; + try { + if (blocking) { + SHMLogger::instance().get(buffer, size); + return true; + } else { + return SHMLogger::instance().tryGet(buffer, size); + } + } catch (const std::exception& e) { + _snprintf_s(buffer, size, _TRUNCATE, "Failed to retrieve log messages: %s", + e.what()); + return false; + } +} + +void SetLogLevel(LogLevel level) +{ + spdlog::get("usvfs")->set_level(ConvertLogLevel(level)); + spdlog::get("hooks")->set_level(ConvertLogLevel(level)); +} + +void WINAPI usvfsUpdateParameters(usvfsParameters* p) +{ + spdlog::get("usvfs")->info("updating parameters:\n" + " . debugMode: {}\n" + " . log level: {}\n" + " . dump type: {}\n" + " . dump path: {}\n" + " . delay process: {}ms", + p->debugMode, usvfsLogLevelToString(p->logLevel), + usvfsCrashDumpTypeToString(p->crashDumpsType), + p->crashDumpsPath, p->delayProcessMs); + + // update actual values used: + usvfs_dump_type = p->crashDumpsType; + usvfs_dump_path = + ush::string_cast(p->crashDumpsPath, ush::CodePage::UTF8); + SetLogLevel(p->logLevel); + + // update parameters in context so spawned process will inherit changes: + context->setDebugParameters(p->logLevel, p->crashDumpsType, p->crashDumpsPath, + std::chrono::milliseconds(p->delayProcessMs)); +} + +// +// Structured Exception handling +// + +std::wstring generate_minidump_name(const wchar_t* dumpPath) +{ + DWORD pid = GetCurrentProcessId(); + wchar_t pname[100]; + if (GetModuleBaseName(GetCurrentProcess(), NULL, pname, _countof(pname)) == 0) + return std::wstring(); + + // find an available name: + wchar_t dmpFile[MAX_PATH]; + int count = 0; + _snwprintf_s(dmpFile, _TRUNCATE, L"%s\\%s-%lu.dmp", dumpPath, pname, pid); + while (winapi::ex::wide::fileExists(dmpFile)) { + if (++count > 99) + return std::wstring(); + _snwprintf_s(dmpFile, _TRUNCATE, L"%s\\%s-%lu_%02d.dmp", dumpPath, pname, pid, + count); + } + return dmpFile; +} + +int createMiniDumpImpl(PEXCEPTION_POINTERS exceptionPtrs, CrashDumpsType type, + const wchar_t* dumpPath, HMODULE dbgDLL) +{ + typedef BOOL(WINAPI * FuncMiniDumpWriteDump)( + HANDLE process, DWORD pid, HANDLE file, MINIDUMP_TYPE dumpType, + const PMINIDUMP_EXCEPTION_INFORMATION exceptionParam, + const PMINIDUMP_USER_STREAM_INFORMATION userStreamParam, + const PMINIDUMP_CALLBACK_INFORMATION callbackParam); + + // notice we avoid logging here on purpose because this is called from the VEHandler + // and the logger can crash it in extreme cases. + // additionally it is also called for MO crashes which use it's own logging. + winapi::ex::wide::createPath(dumpPath); + + auto dmpName = generate_minidump_name(dumpPath); + if (dmpName.empty()) + return 4; + + FuncMiniDumpWriteDump funcDump = reinterpret_cast( + GetProcAddress(dbgDLL, "MiniDumpWriteDump")); + if (!funcDump) + return 5; + + HANDLE dumpFile = winapi::wide::createFile(dmpName) + .createAlways() + .access(GENERIC_WRITE) + .share(FILE_SHARE_WRITE)(); + if (dumpFile != INVALID_HANDLE_VALUE) { + DWORD dumpType = MiniDumpNormal | MiniDumpWithHandleData | + MiniDumpWithUnloadedModules | MiniDumpWithProcessThreadData; + if (type == CrashDumpsType::Data) + dumpType |= MiniDumpWithDataSegs; + if (type == CrashDumpsType::Full) + dumpType |= MiniDumpWithFullMemory; + + _MINIDUMP_EXCEPTION_INFORMATION exceptionInfo; + exceptionInfo.ThreadId = GetCurrentThreadId(); + exceptionInfo.ExceptionPointers = exceptionPtrs; + exceptionInfo.ClientPointers = FALSE; + + BOOL success = funcDump(GetCurrentProcess(), GetCurrentProcessId(), dumpFile, + static_cast(dumpType), &exceptionInfo, + nullptr, nullptr); + + CloseHandle(dumpFile); + + return success ? 0 : 7; + } else + return 6; +} + +int WINAPI usvfsCreateMiniDump(PEXCEPTION_POINTERS exceptionPtrs, CrashDumpsType type, + const wchar_t* dumpPath) +{ + if (type == CrashDumpsType::None) + return 0; + + int res = 1; + if (HMODULE dbgDLL = LoadLibraryW(L"dbghelp.dll")) { + try { + res = createMiniDumpImpl(exceptionPtrs, type, dumpPath, dbgDLL); + } catch (...) { + res = 2; + } + FreeLibrary(dbgDLL); + } + return res; +} + +static bool exceptionInUSVFS(PEXCEPTION_POINTERS exceptionPtrs) +{ + if (!dllModule) // shouldn't happen, check just in case + return true; // create dump to better understand how this could happen + + std::pair range = winapi::ex::getSectionRange(dllModule); + + uintptr_t exceptionAddress = + reinterpret_cast(exceptionPtrs->ExceptionRecord->ExceptionAddress); + + return range.first <= exceptionAddress && exceptionAddress < range.second; +} + +LONG WINAPI VEHandler(PEXCEPTION_POINTERS exceptionPtrs) +{ + // NOTICE: don't use logger in VEHandler as it can cause another fault causing + // VEHandler to be called again and so on. + + if ((exceptionPtrs->ExceptionRecord->ExceptionCode < 0x80000000) // non-critical + || + (exceptionPtrs->ExceptionRecord->ExceptionCode == 0xe06d7363)) { // cpp exception + // don't report non-critical exceptions + return EXCEPTION_CONTINUE_SEARCH; + } + /* + if (((exceptionPtrs->ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE) != 0) + || (exceptionPtrs->ExceptionRecord->ExceptionCode == 0xe06d7363)) { + // don't want to break on non-critical exceptions. 0xe06d7363 indicates a C++ + exception. why are those marked non-continuable? return EXCEPTION_CONTINUE_SEARCH; + } + */ + + // VEHandler is called on "first-chance" exceptions which might be caught and handled. + // Ideally we would like to use an UnhandledExceptionFilter but that fails to catch + // crashes inside our hooks at least on x64, which is the main reason why want a crash + // collection from usvfs. As a workaround/compromise we catch vectored exception but + // only ones that originate directly within the usvfs code: + if (!exceptionInUSVFS(exceptionPtrs)) + return EXCEPTION_CONTINUE_SEARCH; + + // disable our hooking mechanism to increase chances the dump writing won't crash + HookLib::TrampolinePool& trampPool = HookLib::TrampolinePool::instance(); + if (&trampPool) { // need to test this in case of crash before TrampolinePool + // initialized + trampPool.forceUnlockBarrier(); + trampPool.setBlock(true); + } + + usvfsCreateMiniDump(exceptionPtrs, usvfs_dump_type, usvfs_dump_path.c_str()); + + return EXCEPTION_CONTINUE_SEARCH; +} + +// +// Exported functions +// + +void __cdecl InitHooks(LPVOID parameters, size_t) +{ + InitLoggingInternal(false, true); + + const usvfsParameters* params = reinterpret_cast(parameters); + + // there is already a wait in the constructor of HookManager, but this one is useful + // to debug code here (from experience... ), should not wait twice since the second + // will return true immediately + if (params->debugMode) { + while (!::IsDebuggerPresent()) { + // wait for debugger to attach + ::Sleep(100); + } + } + + usvfs_dump_type = params->crashDumpsType; + usvfs_dump_path = + ush::string_cast(params->crashDumpsPath, ush::CodePage::UTF8); + + if (params->delayProcessMs > 0) { + ::Sleep(static_cast(params->delayProcessMs)); + } + + SetLogLevel(params->logLevel); + + if (exceptionHandler == nullptr) { + if (usvfs_dump_type != CrashDumpsType::None) + exceptionHandler = ::AddVectoredExceptionHandler(0, VEHandler); + } else { + spdlog::get("usvfs")->info("vectored exception handler already active"); + // how did this happen?? + } + + spdlog::get("usvfs")->info( + "inithooks called {0} in process {1}:{2} (log level {3}, dump type {4}, dump " + "path {5})", + params->instanceName, winapi::ansi::getModuleFileName(nullptr), + ::GetCurrentProcessId(), static_cast(params->logLevel), + static_cast(params->crashDumpsType), params->crashDumpsPath); + + try { + manager = new usvfs::HookManager(*params, dllModule); + + auto context = manager->context(); + auto exePath = boost::dll::program_location(); + auto libraries = context->librariesToForceLoad(exePath.filename().c_str()); + for (auto library : libraries) { + if (std::filesystem::exists(library)) { + const auto ret = LoadLibraryExW(library.c_str(), NULL, 0); + if (ret) { + spdlog::get("usvfs")->info("inithooks succeeded to force load {0}", + ush::string_cast(library).c_str()); + } else { + spdlog::get("usvfs")->critical( + "inithooks failed to force load {0}", + ush::string_cast(library).c_str()); + } + } + } + + spdlog::get("usvfs")->info("inithooks in process {0} successful", + ::GetCurrentProcessId()); + + } catch (const std::exception& e) { + spdlog::get("usvfs")->debug("failed to initialise hooks: {0}", e.what()); + } +} + +void WINAPI usvfsGetCurrentVFSName(char* buffer, size_t size) +{ + ush::strncpy_sz(buffer, context->callParameters().currentSHMName, size); +} + +BOOL WINAPI usvfsCreateVFS(const usvfsParameters* p) +{ + usvfs::HookContext::remove(p->instanceName); + return usvfsConnectVFS(p); +} + +BOOL WINAPI usvfsConnectVFS(const usvfsParameters* params) +{ + if (spdlog::get("usvfs").get() == nullptr) { + // create temporary logger so we don't get null-pointer exceptions + spdlog::create("usvfs"); + } + + try { + usvfsDisconnectVFS(); + context = new usvfs::HookContext(*params, dllModule); + + return TRUE; + } catch (const std::exception& e) { + spdlog::get("usvfs")->debug("failed to connect to vfs: {}", e.what()); + return FALSE; + } +} + +void WINAPI usvfsDisconnectVFS() +{ + if (spdlog::get("usvfs").get() == nullptr) { + // create temporary logger so we don't get null-pointer exceptions + spdlog::create("usvfs"); + } + + spdlog::get("usvfs")->debug("remove from process {}", GetCurrentProcessId()); + + if (manager != nullptr) { + delete manager; + manager = nullptr; + } + + if (context != nullptr) { + delete context; + context = nullptr; + spdlog::get("usvfs")->debug("vfs unloaded"); + } +} + +bool processStillActive(DWORD pid) +{ + HANDLE proc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + + if (proc == nullptr) { + return false; + } + + ON_BLOCK_EXIT([&]() { + if (proc != INVALID_HANDLE_VALUE) + ::CloseHandle(proc); + }); + + DWORD exitCode; + if (!GetExitCodeProcess(proc, &exitCode)) { + spdlog::get("usvfs")->warn("failed to query exit code on process {}: {}", pid, + ::GetLastError()); + return false; + } else { + return exitCode == STILL_ACTIVE; + } +} + +BOOL WINAPI usvfsGetVFSProcessList(size_t* count, LPDWORD processIDs) +{ + if (count == nullptr) { + SetLastError(ERROR_INVALID_PARAMETER); + return FALSE; + } + + if (context == nullptr) { + *count = 0; + } else { + std::vector pids = context->registeredProcesses(); + size_t realCount = 0; + for (DWORD pid : pids) { + if (processStillActive(pid)) { + if ((realCount < *count) && (processIDs != nullptr)) { + processIDs[realCount] = pid; + } + + ++realCount; + } // else the process has already ended + } + *count = realCount; + } + return TRUE; +} + +BOOL WINAPI usvfsGetVFSProcessList2(size_t* count, DWORD** buffer) +{ + if (!count || !buffer) { + SetLastError(ERROR_INVALID_PARAMETER); + return FALSE; + } + + *count = 0; + *buffer = nullptr; + + std::vector pids = context->registeredProcesses(); + auto last = std::remove_if(pids.begin(), pids.end(), [](DWORD id) { + return !processStillActive(id); + }); + + pids.erase(last, pids.end()); + + if (pids.empty()) { + return TRUE; + } + + *count = pids.size(); + *buffer = static_cast(std::calloc(pids.size(), sizeof(DWORD))); + + std::copy(pids.begin(), pids.end(), *buffer); + + return TRUE; +} + +void WINAPI usvfsClearVirtualMappings() +{ + context->redirectionTable()->clear(); + context->inverseTable()->clear(); +} + +/// ensure the specified path exists. If a physical path of the same name +/// exists, it is inserted into the virtual directory as an empty reference. If +/// the path doesn't exist virtually and can't be cloned from a physical +/// directory, this returns false +/// \todo if this fails (i.e. not all intermediate directories exists) any +/// intermediate directories already created aren't removed +bool assertPathExists(usvfs::RedirectionTreeContainer& table, LPCWSTR path) +{ + bfs::path p(path); + p = p.parent_path(); + + usvfs::RedirectionTree::NodeT* current = table.get(); + + for (auto iter = p.begin(); iter != p.end(); iter = ush::nextIter(iter, p.end())) { + if (current->exists(iter->string().c_str())) { + // subdirectory exists virtually, all good + usvfs::RedirectionTree::NodePtrT found = current->node(iter->string().c_str()); + current = found.get().get(); + } else { + // targetPath is relative to the last rerouted "real" path. This means + // that if virtual c:/foo maps to real c:/windows then creating virtual + // c:/foo/bar will map to real c:/windows/bar + bfs::path targetPath = current->data().linkTarget.size() > 0 + ? bfs::path(current->data().linkTarget.c_str()) / *iter + : *iter / "\\"; + + // is_directory returns false for symlinks and reparse points, + // which causes this function to fail if the target path contains + // either of those. paths containing reparse points is a common + // scenario when running under Wine, so check for those explicitly. + // this check could have a false positive if the path contains a + // symlink to a file, but such a scenario is extremely unlikely. + if (is_directory(targetPath) || is_symlink(targetPath) || + status(targetPath).type() == bfs::file_type::reparse_file) { + usvfs::RedirectionTree::NodePtrT newNode = + table.addDirectory(current->path() / *iter, targetPath.string().c_str(), + ush::FLAG_DUMMY, false); + current = newNode.get().get(); + } else { + spdlog::get("usvfs")->info("{} doesn't exist", targetPath.c_str()); + return false; + } + } + } + + return true; +} + +static bool fileNameInSkipSuffixes(const std::string& fileNameUtf8, + const std::vector& skipFileSuffixes) +{ + for (const auto& skipFileSuffix : skipFileSuffixes) { + if (boost::algorithm::iends_with(fileNameUtf8, skipFileSuffix)) { + spdlog::get("usvfs")->debug( + "file '{}' should be skipped, matches file suffix '{}'", fileNameUtf8, + skipFileSuffix); + return true; + } + } + return false; +} + +static bool fileNameInSkipDirectories(const std::string& directoryNameUtf8, + const std::vector& skipDirectories) +{ + for (const auto& skipDir : skipDirectories) { + if (boost::algorithm::iequals(directoryNameUtf8, skipDir)) { + spdlog::get("usvfs")->debug("directory '{}' should be skipped", + directoryNameUtf8); + return true; + } + } + return false; +} + +BOOL WINAPI usvfsVirtualLinkFile(LPCWSTR source, LPCWSTR destination, + unsigned int flags) +{ + // TODO difference between winapi and ntdll api regarding system32 vs syswow64 + // (and other windows links?) + try { + if (!assertPathExists(context->redirectionTable(), destination)) { + SetLastError(ERROR_PATH_NOT_FOUND); + return FALSE; + } + + const auto skipFileSuffixes = context->skipFileSuffixes(); + + std::string sourceU8 = ush::string_cast(source, ush::CodePage::UTF8); + + // Check if the file should be skipped + if (fileNameInSkipSuffixes(sourceU8, skipFileSuffixes)) { + // return false when we want to fail when the file is skipped + return (flags & LINKFLAG_FAILIFSKIPPED) ? FALSE : TRUE; + } + + auto res = context->redirectionTable().addFile( + bfs::path(destination), usvfs::RedirectionDataLocal(sourceU8), + !(flags & LINKFLAG_FAILIFEXISTS)); + + if (shouldAddToInverseTree(sourceU8)) { + std::string destinationU8 = + ush::string_cast(destination, ush::CodePage::UTF8); + + context->inverseTable().addFile(bfs::path(source), + usvfs::RedirectionDataLocal(destinationU8), true); + } + + context->updateParameters(); + + if (res.get() == nullptr) { + // the tree structure currently doesn't provide useful error codes but + // this is currently the only reason + // we would return a nullptr. + SetLastError(ERROR_FILE_EXISTS); + return FALSE; + } else { + return TRUE; + } + } catch (const std::exception& e) { + spdlog::get("usvfs")->error("failed to copy file {}", e.what()); + // TODO: no clue what's wrong + SetLastError(ERROR_INVALID_DATA); + return FALSE; + } +} + +/** + * @brief extract the flags relevant to redirection + */ +static usvfs::shared::TreeFlags convertRedirectionFlags(unsigned int flags) +{ + usvfs::shared::TreeFlags result = 0; + if (flags & LINKFLAG_CREATETARGET) { + result |= usvfs::shared::FLAG_CREATETARGET; + } + return result; +} + +BOOL WINAPI usvfsVirtualLinkDirectoryStatic(LPCWSTR source, LPCWSTR destination, + unsigned int flags) +{ + // TODO change notification not yet implemented + try { + if ((flags & LINKFLAG_FAILIFEXISTS) && winapi::ex::wide::fileExists(destination)) { + SetLastError(ERROR_FILE_EXISTS); + return FALSE; + } + + if (!assertPathExists(context->redirectionTable(), destination)) { + SetLastError(ERROR_PATH_NOT_FOUND); + return FALSE; + } + + std::string sourceU8 = + ush::string_cast(source, ush::CodePage::UTF8) + "\\"; + + context->redirectionTable().addDirectory( + destination, usvfs::RedirectionDataLocal(sourceU8), + usvfs::shared::FLAG_DIRECTORY | convertRedirectionFlags(flags), + (flags & LINKFLAG_CREATETARGET) != 0); + + const auto skipDirectories = context->skipDirectories(); + const auto skipFileSuffixes = context->skipFileSuffixes(); + + if ((flags & LINKFLAG_RECURSIVE) != 0) { + std::wstring sourceP(source); + std::wstring sourceW = sourceP + L"\\"; + std::wstring destinationW = std::wstring(destination) + L"\\"; + if (sourceP.length() >= MAX_PATH && !ush::startswith(sourceP.c_str(), LR"(\\?\)")) + sourceP = LR"(\\?\)" + sourceP; + + for (winapi::ex::wide::FileResult file : + winapi::ex::wide::quickFindFiles(sourceP.c_str(), L"*")) { + if (file.attributes & FILE_ATTRIBUTE_DIRECTORY) { + if ((file.fileName != L".") && (file.fileName != L"..")) { + + const auto nameU8 = ush::string_cast(file.fileName.c_str(), + ush::CodePage::UTF8); + // Check if the directory should be skipped + if (fileNameInSkipDirectories(nameU8, skipDirectories)) { + // Fail if we desire to fail when a dir/file is skipped + if (flags & LINKFLAG_FAILIFSKIPPED) { + spdlog::get("usvfs")->debug( + "directory '{}' skipped, failing as defined by link flags", nameU8); + return FALSE; + } + + continue; + } + + usvfsVirtualLinkDirectoryStatic((sourceW + file.fileName).c_str(), + (destinationW + file.fileName).c_str(), + flags); + } + } else { + const auto nameU8 = + ush::string_cast(file.fileName.c_str(), ush::CodePage::UTF8); + + // Check if the file should be skipped + if (fileNameInSkipSuffixes(nameU8, skipFileSuffixes)) { + // Fail if we desire to fail when a dir/file is skipped + if (flags & LINKFLAG_FAILIFSKIPPED) { + spdlog::get("usvfs")->debug( + "file '{}' skipped, failing as defined by link flags", nameU8); + return FALSE; + } + + continue; + } + + // TODO could save memory here by storing only the file name for the + // source and constructing the full name using the parent directory + context->redirectionTable().addFile( + bfs::path(destination) / nameU8, + usvfs::RedirectionDataLocal(sourceU8 + nameU8), true); + + if (shouldAddToInverseTree(nameU8)) { + std::string destinationU8 = + ush::string_cast(destination, ush::CodePage::UTF8) + "\\"; + + context->inverseTable().addFile( + bfs::path(source) / nameU8, + usvfs::RedirectionDataLocal(destinationU8 + nameU8), true); + } + } + } + } + + context->updateParameters(); + + return TRUE; + } catch (const std::exception& e) { + spdlog::get("usvfs")->error("failed to copy file {}", e.what()); + // TODO: no clue what's wrong + SetLastError(ERROR_INVALID_DATA); + return FALSE; + } +} + +BOOL WINAPI usvfsCreateProcessHooked(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, + LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, + LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation) +{ + BOOL susp = dwCreationFlags & CREATE_SUSPENDED; + DWORD flags = dwCreationFlags | CREATE_SUSPENDED; + + BOOL blacklisted = context->executableBlacklisted(lpApplicationName, lpCommandLine); + + BOOL res = CreateProcessW(lpApplicationName, lpCommandLine, lpProcessAttributes, + lpThreadAttributes, bInheritHandles, flags, lpEnvironment, + lpCurrentDirectory, lpStartupInfo, lpProcessInformation); + if (!res) { + spdlog::get("usvfs")->error("failed to spawn {}", + ush::string_cast(lpCommandLine)); + return FALSE; + } + + if (!blacklisted) { + std::wstring applicationDirPath = winapi::wide::getModuleFileName(dllModule); + boost::filesystem::path p(applicationDirPath); + try { + usvfs::injectProcess(p.parent_path().wstring(), context->callParameters(), + *lpProcessInformation); + } catch (const std::exception& e) { + spdlog::get("usvfs")->error("failed to inject: {}", e.what()); + logExtInfo(e, LogLevel::Error); + ::TerminateProcess(lpProcessInformation->hProcess, 1); + ::SetLastError(ERROR_INVALID_PARAMETER); + return FALSE; + } + } + + if (!susp) { + ResumeThread(lpProcessInformation->hThread); + } + + return TRUE; +} + +BOOL WINAPI usvfsCreateVFSDump(LPSTR buffer, size_t* size) +{ + assert(size != nullptr); + std::ostringstream output; + usvfs::shared::dumpTree(output, *context->redirectionTable().get()); + std::string str = output.str(); + if ((buffer != NULL) && (*size > 0)) { + strncpy_s(buffer, *size, str.c_str(), _TRUNCATE); + } + bool success = *size >= str.length(); + *size = str.length(); + return success ? TRUE : FALSE; +} + +VOID WINAPI usvfsBlacklistExecutable(LPCWSTR executableName) +{ + context->blacklistExecutable(executableName); +} + +VOID WINAPI usvfsClearExecutableBlacklist() +{ + context->clearExecutableBlacklist(); +} + +VOID WINAPI usvfsAddSkipFileSuffix(LPCWSTR fileSuffix) +{ + context->addSkipFileSuffix(fileSuffix); +} + +VOID WINAPI usvfsClearSkipFileSuffixes() +{ + context->clearSkipFileSuffixes(); +} + +VOID WINAPI usvfsAddSkipDirectory(LPCWSTR directory) +{ + context->addSkipDirectory(directory); +} + +VOID WINAPI usvfsClearSkipDirectories() +{ + context->clearSkipDirectories(); +} + +VOID WINAPI usvfsForceLoadLibrary(LPCWSTR processName, LPCWSTR libraryPath) +{ + context->forceLoadLibrary(processName, libraryPath); +} + +VOID WINAPI usvfsClearLibraryForceLoads() +{ + context->clearLibraryForceLoads(); +} + +VOID WINAPI usvfsPrintDebugInfo() +{ + spdlog::get("usvfs")->warn("===== debug {} =====", + context->redirectionTable().shmName()); + void* buffer = nullptr; + size_t bufferSize = 0; + context->redirectionTable().getBuffer(buffer, bufferSize); + std::ostringstream temp; + for (size_t i = 0; i < bufferSize; ++i) { + temp << std::hex << std::setfill('0') << std::setw(2) + << (unsigned)reinterpret_cast(buffer)[i] << " "; + if ((i % 16) == 15) { + spdlog::get("usvfs")->info("{}", temp.str()); + temp.str(""); + temp.clear(); + } + } + if (!temp.str().empty()) { + spdlog::get("usvfs")->info("{}", temp.str()); + } + spdlog::get("usvfs")->warn("===== / debug {} =====", + context->redirectionTable().shmName()); +} + +const char* WINAPI usvfsVersionString() +{ + return USVFS_VERSION_STRING; +} + +// +// DllMain +// + +BOOL APIENTRY DllMain(HMODULE module, DWORD reasonForCall, LPVOID) +{ + switch (reasonForCall) { + case DLL_PROCESS_ATTACH: { + dllModule = module; + } break; + case DLL_PROCESS_DETACH: { + if (exceptionHandler) + ::RemoveVectoredExceptionHandler(exceptionHandler); + } break; + case DLL_THREAD_ATTACH: { + } break; + case DLL_THREAD_DETACH: { + } break; + } + + return TRUE; +} diff --git a/libs/usvfs/src/usvfs_dll/usvfsparameters.cpp b/libs/usvfs/src/usvfs_dll/usvfsparameters.cpp new file mode 100644 index 0000000..7059ec2 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/usvfsparameters.cpp @@ -0,0 +1,193 @@ +#include "usvfsparametersprivate.h" +#include + +usvfsParameters::usvfsParameters() + : debugMode(false), logLevel(LogLevel::Debug), crashDumpsType(CrashDumpsType::None), + delayProcessMs(0) +{ + std::fill(std::begin(instanceName), std::end(instanceName), 0); + std::fill(std::begin(currentSHMName), std::end(currentSHMName), 0); + std::fill(std::begin(currentInverseSHMName), std::end(currentInverseSHMName), 0); + std::fill(std::begin(crashDumpsPath), std::end(crashDumpsPath), 0); +} + +usvfsParameters::usvfsParameters(const char* instanceName, const char* currentSHMName, + const char* currentInverseSHMName, bool debugMode, + LogLevel logLevel, CrashDumpsType crashDumpsType, + const char* crashDumpsPath, int delayProcessMs) + : usvfsParameters() +{ + strncpy_s(this->instanceName, instanceName, _TRUNCATE); + strncpy_s(this->currentSHMName, currentSHMName, _TRUNCATE); + strncpy_s(this->currentInverseSHMName, currentInverseSHMName, _TRUNCATE); + this->debugMode = debugMode; + this->logLevel = logLevel; + this->crashDumpsType = crashDumpsType; + strncpy_s(this->crashDumpsPath, crashDumpsPath, _TRUNCATE); + this->delayProcessMs = delayProcessMs; +} + +usvfsParameters::usvfsParameters(const USVFSParameters& oldParams) + : usvfsParameters(oldParams.instanceName, oldParams.currentSHMName, + oldParams.currentInverseSHMName, oldParams.debugMode, + oldParams.logLevel, oldParams.crashDumpsType, + oldParams.crashDumpsPath, 0) +{} + +void usvfsParameters::setInstanceName(const char* name) +{ + strncpy_s(instanceName, name, _TRUNCATE); + strncpy_s(currentSHMName, 60, name, _TRUNCATE); + memset(currentInverseSHMName, '\0', _countof(currentInverseSHMName)); + _snprintf(currentInverseSHMName, 60, "inv_%s", name); +} + +void usvfsParameters::setDebugMode(bool b) +{ + debugMode = b; +} + +void usvfsParameters::setLogLevel(LogLevel level) +{ + logLevel = level; +} + +void usvfsParameters::setCrashDumpType(CrashDumpsType type) +{ + crashDumpsType = type; +} + +void usvfsParameters::setCrashDumpPath(const char* path) +{ + if (path && *path && strlen(path) < _countof(crashDumpsPath)) { + memcpy(crashDumpsPath, path, strlen(path) + 1); + } else { + // crashDumpsPath invalid or overflow of USVFSParameters variable so disable + // crash dumps: + crashDumpsPath[0] = 0; + crashDumpsType = CrashDumpsType::None; + } +} + +void usvfsParameters::setProcessDelay(int milliseconds) +{ + delayProcessMs = milliseconds; +} + +extern "C" +{ + + const char* usvfsLogLevelToString(LogLevel lv) + { + switch (lv) { + case LogLevel::Debug: + return "debug"; + + case LogLevel::Info: + return "info"; + + case LogLevel::Warning: + return "warning"; + + case LogLevel::Error: + return "error"; + + default: + return "unknown"; + } + } + + const char* usvfsCrashDumpTypeToString(CrashDumpsType t) + { + switch (t) { + case CrashDumpsType::None: + return "none"; + + case CrashDumpsType::Mini: + return "mini"; + + case CrashDumpsType::Data: + return "data"; + + case CrashDumpsType::Full: + return "full"; + + default: + return "unknown"; + } + } + + usvfsParameters* usvfsCreateParameters() + { + return new (std::nothrow) usvfsParameters; + } + + usvfsParameters* usvfsDupeParameters(usvfsParameters* p) + { + if (!p) { + return nullptr; + } + + auto* dupe = usvfsCreateParameters(); + if (!dupe) { + return nullptr; + } + + *dupe = *p; + + return dupe; + } + + void usvfsCopyParameters(const usvfsParameters* source, usvfsParameters* dest) + { + *dest = *source; + } + + void usvfsFreeParameters(usvfsParameters* p) + { + delete p; + } + + void usvfsSetInstanceName(usvfsParameters* p, const char* name) + { + if (p) { + p->setInstanceName(name); + } + } + + void usvfsSetDebugMode(usvfsParameters* p, BOOL debugMode) + { + if (p) { + p->setDebugMode(debugMode); + } + } + + void usvfsSetLogLevel(usvfsParameters* p, LogLevel level) + { + if (p) { + p->setLogLevel(level); + } + } + + void usvfsSetCrashDumpType(usvfsParameters* p, CrashDumpsType type) + { + if (p) { + p->setCrashDumpType(type); + } + } + + void usvfsSetCrashDumpPath(usvfsParameters* p, const char* path) + { + if (p) { + p->setCrashDumpPath(path); + } + } + + void usvfsSetProcessDelay(usvfsParameters* p, int milliseconds) + { + if (p) { + p->setProcessDelay(milliseconds); + } + } + +} // extern "C" diff --git a/libs/usvfs/src/usvfs_dll/version.rc b/libs/usvfs/src/usvfs_dll/version.rc new file mode 100644 index 0000000..7dbf8e3 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/version.rc @@ -0,0 +1,40 @@ +#include "Winver.h" +#include "..\..\include\usvfs\usvfs_version.h" + +#define VER_FILEVERSION USVFS_VERSION_MAJOR,USVFS_VERSION_MINOR,USVFS_VERSION_BUILD,USVFS_VERSION_REVISION +#define VER_FILEVERSION_STR USVFS_VERSION_STRING + +#define VER_PRODUCTVERSION USVFS_VERSION_MAJOR,USVFS_VERSION_MINOR,USVFS_VERSION_BUILD,USVFS_VERSION_REVISION +#define VER_PRODUCTVERSION_STR USVFS_VERSION_STRING + +VS_VERSION_INFO VERSIONINFO +FILEVERSION VER_FILEVERSION +PRODUCTVERSION VER_PRODUCTVERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS (0) +FILEOS VOS__WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "CompanyName", "Mod Organizer 2 Team\0" + VALUE "FileDescription", "USVFS\0" +#ifdef _WIN64 + VALUE "OriginalFilename", "usvfs_x64.dll\0" +#else + VALUE "OriginalFilename", "usvfs_x86.dll\0" +#endif + VALUE "ProductName", "USVFS\0" + VALUE "ProductVersion", VER_PRODUCTVERSION_STR + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409L, 1200 + END +END diff --git a/libs/usvfs/src/usvfs_helper/CMakeLists.txt b/libs/usvfs/src/usvfs_helper/CMakeLists.txt new file mode 100644 index 0000000..5e083d9 --- /dev/null +++ b/libs/usvfs/src/usvfs_helper/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(usvfs_helper STATIC inject.h inject.cpp) +target_link_libraries(usvfs_helper PUBLIC shared PRIVATE tinjectlib) +target_include_directories(usvfs_helper PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +set_target_properties(usvfs_helper PROPERTIES FOLDER injection) diff --git a/libs/usvfs/src/usvfs_helper/inject.cpp b/libs/usvfs/src/usvfs_helper/inject.cpp new file mode 100644 index 0000000..bb1cb58 --- /dev/null +++ b/libs/usvfs/src/usvfs_helper/inject.cpp @@ -0,0 +1,193 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#include +#include + +#include + +#include + +#include "inject.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ush = usvfs::shared; + +using namespace winapi; + +void usvfs::injectProcess(const std::wstring& applicationPath, + const usvfsParameters& parameters, + const PROCESS_INFORMATION& processInfo) +{ + injectProcess(applicationPath, parameters, processInfo.hProcess, processInfo.hThread); +} + +void usvfs::injectProcess(const std::wstring& applicationPath, + const usvfsParameters& parameters, HANDLE processHandle, + HANDLE threadHandle) +{ + bool proc64 = false; + bool sameBitness = false; + { + SYSTEM_INFO info; + GetSystemInfo(&info); + BOOL wow64; + IsWow64Process(processHandle, &wow64); + if (wow64) { + // process is running under wow64 so it has to be a 32bit process running on 64bit + // windows + proc64 = false; + BOOL temp; + IsWow64Process(GetCurrentProcess(), &temp); + sameBitness = temp == TRUE; + } else { + BOOL selfWow64; + IsWow64Process(GetCurrentProcess(), &selfWow64); + if (selfWow64) { + // WE are a 32 bit process running on 64bit windows. the other process isn't, so + // its 64bit + proc64 = true; + } else { + sameBitness = true; + // we have the same bitness as that other process, but which is it? +#ifdef _WIN64 + proc64 = true; +#else + proc64 = false; +#endif + } + } + } + boost::filesystem::path binPath = boost::filesystem::path(applicationPath); + spdlog::get("usvfs")->info("injecting to process {} with {} bitness", + ::GetProcessId(processHandle), + sameBitness ? "same" : "different"); + + if (sameBitness) { + static constexpr auto USVFS_DLL = +#ifdef _WIN64 + L"usvfs_x64.dll"; +#else + L"usvfs_x86.dll"; +#endif + const auto& preferedDll = binPath / USVFS_DLL; + boost::filesystem::path dllPath = preferedDll; + bool dllFound = boost::filesystem::exists(dllPath); + // support for runing tests using a usvfs dll in lib folder (and proxy under bin): + if (!dllFound && binPath.filename() == L"bin") { + dllPath = binPath.parent_path() / L"lib" / USVFS_DLL; + dllFound = boost::filesystem::exists(dllPath); + } + if (!dllFound) { + USVFS_THROW_EXCEPTION( + file_not_found_error() + << ex_msg(std::string("dll missing: ") + + ush::string_cast(preferedDll.wstring()).c_str())); + } + + spdlog::get("usvfs")->info("dll path: {}", dllPath.wstring()); + + InjectLib::InjectDLL(processHandle, threadHandle, dllPath.c_str(), "InitHooks", + ¶meters, sizeof(parameters)); + + spdlog::get("usvfs")->info("injection to same bitness process {} successful", + ::GetProcessId(processHandle)); + } else { + // first try platform specific proxy exe: + static constexpr auto USVFS_PREFERED_EXE = +#ifdef _WIN64 + L"usvfs_proxy_x86.exe"; +#else + L"usvfs_proxy_x64.exe"; +#endif + const auto& preferedExe = binPath / USVFS_PREFERED_EXE; + boost::filesystem::path exePath = preferedExe; + bool exeFound = boost::filesystem::exists(exePath); + // support for runing tests using a usvfs dll in lib folder (and proxy under bin): + if (!exeFound && binPath.filename() == L"lib") { + exePath = binPath.parent_path() / L"bin" / USVFS_PREFERED_EXE; + exeFound = boost::filesystem::exists(exePath); + } + // finally fallback to old proxy naming (but only for 64bit as we don't have a 64bit + // proxy in this case): +#ifdef _WIN64 + if (!exeFound) { + exePath = binPath / L"usvfs_proxy.exe"; + exeFound = boost::filesystem::exists(exePath); + } +#endif + if (!exeFound) { + USVFS_THROW_EXCEPTION(file_not_found_error() << ex_msg( + std::string("usvfs proxy not found: ") + + ush::string_cast(preferedExe.wstring()))); + } else + spdlog::get("usvfs")->info("using usvfs proxy: {}", + ush::string_cast(preferedExe.wstring())); + // need to use proxy aplication to inject + auto proxyProcess = + std::move(wide::createProcess(exePath.wstring()) + .arg(L"--instance") + .arg(ush::string_cast(parameters.instanceName)) + .arg(L"--pid") + .arg(GetProcessId(processHandle))); + + if (threadHandle != INVALID_HANDLE_VALUE) { + proxyProcess.arg("--tid").arg(GetThreadId(threadHandle)); + } + process::Result result = proxyProcess(); + if (!result.valid) { + USVFS_THROW_EXCEPTION(unknown_error() + << ex_msg(std::string("failed to start proxy ") + + ush::string_cast(exePath.wstring())) + << ex_win_errcode(result.errorCode)); + } else { + // wait for proxy completion. this shouldn't take long, 15 seconds is very + // generous + switch (WaitForSingleObject(result.processInfo.hProcess, 15000)) { + case WAIT_TIMEOUT: { + spdlog::get("usvfs")->debug("proxy timeout"); + TerminateProcess(result.processInfo.hProcess, 1); + USVFS_THROW_EXCEPTION(timeout_error() + << ex_msg(std::string("proxy didn't complete in time"))); + } break; + case WAIT_FAILED: { + spdlog::get("usvfs")->debug("proxy wait failed"); + TerminateProcess(result.processInfo.hProcess, 1); + USVFS_THROW_EXCEPTION(unknown_error() + << ex_msg( + std::string("failed to wait for proxy completion")) + << ex_win_errcode(result.errorCode)); + } break; + default: { + spdlog::get("usvfs")->debug("proxy run successful"); + // nop + } break; + } + } + } +} diff --git a/libs/usvfs/src/usvfs_helper/inject.h b/libs/usvfs/src/usvfs_helper/inject.h new file mode 100644 index 0000000..10f179f --- /dev/null +++ b/libs/usvfs/src/usvfs_helper/inject.h @@ -0,0 +1,51 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ +#pragma once + +#include "usvfsparameters.h" +#include +#include + +namespace usvfs +{ + +/** + * @brief inject usvfs to a process + * @param applicationPath + * @param parameters + * @param processInfo + */ +void injectProcess(const std::wstring& applicationPath, + const usvfsParameters& parameters, + const PROCESS_INFORMATION& processInfo); + +/** + * @brief inject usvfs to a process + * @param applicationPath path to usvfs + * @param parameters + * @param process process handle to inject to + * @param thread main thread inside that process. This can be set to + * INVALID_HANDLE_VALUE in which case a new thread is created in the process + */ +void injectProcess(const std::wstring& applicationPath, + const usvfsParameters& parameters, HANDLE process, HANDLE thread); + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_proxy/CMakeLists.txt b/libs/usvfs/src/usvfs_proxy/CMakeLists.txt new file mode 100644 index 0000000..47614ee --- /dev/null +++ b/libs/usvfs/src/usvfs_proxy/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Boost CONFIG REQUIRED COMPONENTS filesystem) + +add_executable(usvfs_proxy main.cpp version.rc) +target_link_libraries(usvfs_proxy PRIVATE usvfs_dll shared usvfs_helper) +set_target_properties(usvfs_proxy + PROPERTIES + RUNTIME_OUTPUT_NAME usvfs_proxy${ARCH_POSTFIX} + RUNTIME_OUTPUT_DIRECTORY_DEBUG ${USVFS_BINDIR} + RUNTIME_OUTPUT_DIRECTORY_RELEASE ${USVFS_BINDIR} +) + +install(TARGETS usvfs_proxy EXPORT usvfs${ARCH_POSTFIX}Targets) +install(FILES $ DESTINATION pdb OPTIONAL) diff --git a/libs/usvfs/src/usvfs_proxy/main.cpp b/libs/usvfs/src/usvfs_proxy/main.cpp new file mode 100644 index 0000000..88d4696 --- /dev/null +++ b/libs/usvfs/src/usvfs_proxy/main.cpp @@ -0,0 +1,221 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ + +#include "pch.h" +#include <../usvfs_dll/hookcontext.h> +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bi = boost::interprocess; +namespace bfs = boost::filesystem; +using usvfs::SharedParameters; +using usvfs::shared::SharedMemoryT; + +template +T getParameter(std::vector& arguments, const std::string& key, + bool consume) +{ + auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key); + if ((iter != arguments.end()) && ((iter + 1) != arguments.end())) { + T result = boost::lexical_cast(*(iter + 1)); + if (consume) { + arguments.erase(iter, iter + 2); + } + return result; + } else { + throw std::runtime_error(std::string("argument missing " + key)); + } +} + +template +T getParameter(std::vector& arguments, const std::string& key, + const T& def, bool consume) +{ + auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key); + if ((iter != arguments.end()) && ((iter + 1) != arguments.end())) { + T result = boost::lexical_cast(*(iter + 1)); + if (consume) { + arguments.erase(iter, iter + 2); + } + return result; + } else { + return def; + } +} + +static void exceptionDialog(int line, int num, ...) +{ + va_list args; + va_start(args, num); + + std::wstring wstr; + WCHAR buf[256]; + wstr.append(L"Unhandled USVFS proxy exception (line "); + wsprintf(buf, L"%d): ", line); + wstr.append(buf); + for (int i = 0; i < num; i++) { + wsprintf(buf, L"%S", va_arg(args, const char*)); + if (i < num - 1) + wsprintf(buf, L", "); + wstr.append(buf); + } + + MessageBox(NULL, wstr.data(), NULL, MB_OK); + + va_end(args); +} + +int main(int argc, char** argv) +{ + std::shared_ptr logger; + + std::vector arguments; + std::copy(argv + 1, argv + argc, std::back_inserter(arguments)); + + std::string instance; + try { + SHMLogger::open("usvfs"); + logger = spdlog::create("usvfs", "usvfs"); + logger->set_pattern("%H:%M:%S.%e [%L] (proxy) %v"); + + instance = getParameter(arguments, "instance", true); + } catch (const std::exception& e) { + if (logger.get() == nullptr) { + exceptionDialog(__LINE__, 1, e.what()); + return 1; + } + try { + logger->critical("{}", e.what()); + } catch (const spdlog::spdlog_ex& e2) { + exceptionDialog(__LINE__, 2, e.what(), e2.what()); + // no way to log this + } catch (const std::exception&) { + logger->critical(e.what()); + } + return 1; + } + + try { + std::string executable = + getParameter(arguments, "executable", "", true); + int pid = getParameter(arguments, "pid", 0, true); + int tid = getParameter(arguments, "tid", 0, true); + + logger->info("instance: {}", instance); + logger->info("exe: {}", executable); + logger->info("pid: {}", pid); + + if (executable.empty() && (pid == 0)) { + logger->warn("not all required settings set"); + return 1; + } + + SharedMemoryT configurationSHM(bi::open_only, instance.c_str()); + if (!configurationSHM.check_sanity()) { + logger->warn("failed to connect to vfs"); + return 1; + } + logger->info("size: {}", configurationSHM.get_size()); + logger->info("addr: {0:p}", configurationSHM.get_address()); + logger->info("objs: {}", configurationSHM.get_num_named_objects()); + std::pair params = + configurationSHM.find("parameters"); + if (params.first == nullptr) { + logger->error("failed to open shared configuration for {}", instance); + return 1; + } + + boost::filesystem::path p(winapi::wide::getModuleFileName(nullptr)); + + if (executable.empty()) { + HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); + HANDLE threadHandle = INVALID_HANDLE_VALUE; + if (tid != 0) { + threadHandle = OpenThread(THREAD_ALL_ACCESS, FALSE, tid); + } + + BOOL blacklisted = FALSE; + TCHAR szModName[MAX_PATH]; + + if (GetModuleFileNameEx(processHandle, NULL, szModName, + sizeof(szModName) / sizeof(TCHAR))) { + const auto appName = usvfs::shared::string_cast(szModName); + + if (params.first->executableBlacklisted(appName, {})) { + logger->info("not injecting {} as application is blacklisted", appName); + blacklisted = TRUE; + } + } + + if (!blacklisted) { + usvfs::injectProcess(p.parent_path().wstring(), params.first->makeLocal(), + processHandle, threadHandle); + } + } else { + winapi::process::Result process = + winapi::ansi::createProcess(executable) + .arguments(arguments.begin(), arguments.end()) + .workingDirectory(bfs::path(executable).parent_path().string()) + .suspended()(); + + if (!process.valid) { + return 1; + } + + BOOL blacklisted = FALSE; + + if (params.first->executableBlacklisted(executable, {})) { + logger->info("not injecting {} as application is blacklisted", executable); + + blacklisted = TRUE; + } + + if (!blacklisted) { + usvfs::injectProcess(p.parent_path().wstring(), params.first->makeLocal(), + process.processInfo); + } + + ResumeThread(process.processInfo.hThread); + } + + return 0; + } catch (const std::exception& e) { + try { + logger->critical("unhandled exception: {}", e.what()); + logExtInfo(e); + } catch (const spdlog::spdlog_ex& e2) { + // no way to log this + exceptionDialog(__LINE__, 2, e.what(), e2.what()); + } catch (const std::exception&) { + logger->critical(e.what()); + } + } +} diff --git a/libs/usvfs/src/usvfs_proxy/version.rc b/libs/usvfs/src/usvfs_proxy/version.rc new file mode 100644 index 0000000..03652c2 --- /dev/null +++ b/libs/usvfs/src/usvfs_proxy/version.rc @@ -0,0 +1,40 @@ +#include "Winver.h" +#include "..\..\include\usvfs\usvfs_version.h" + +#define VER_FILEVERSION USVFS_VERSION_MAJOR,USVFS_VERSION_MINOR,USVFS_VERSION_BUILD,USVFS_VERSION_REVISION +#define VER_FILEVERSION_STR USVFS_VERSION_STRING + +#define VER_PRODUCTVERSION USVFS_VERSION_MAJOR,USVFS_VERSION_MINOR,USVFS_VERSION_BUILD,USVFS_VERSION_REVISION +#define VER_PRODUCTVERSION_STR USVFS_VERSION_STRING + +VS_VERSION_INFO VERSIONINFO +FILEVERSION VER_FILEVERSION +PRODUCTVERSION VER_PRODUCTVERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS (0) +FILEOS VOS__WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "CompanyName", "Mod Organizer 2 Team\0" + VALUE "FileDescription", "USVFS Proxy\0" +#ifdef _WIN64 + VALUE "OriginalFilename", "usvfs_proxy_x64.exe\0" +#else + VALUE "OriginalFilename", "usvfs_proxy_x86.exe\0" +#endif + VALUE "ProductName", "USVFS\0" + VALUE "ProductVersion", VER_PRODUCTVERSION_STR + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409L, 1200 + END +END diff --git a/libs/usvfs/test/CMakeLists.txt b/libs/usvfs/test/CMakeLists.txt new file mode 100644 index 0000000..c34b31f --- /dev/null +++ b/libs/usvfs/test/CMakeLists.txt @@ -0,0 +1,74 @@ +cmake_minimum_required(VERSION 3.16) + +include(CMakeParseArguments) + +#! usvfs_set_test_properties +# +# this function sets the following properties on the given executable or shared +# library test target: +# - OUTPUT_NAME to add arch-specific prefix +# - OUTPUT_DIRECTORY to put the test executable or shared library in the right location +# - FOLDER to organize the VS solution layout +# +# \param:FOLDER if present, specifies the subfolder to use in the solution +# +function(usvfs_set_test_properties TARGET) + cmake_parse_arguments(USVFS_TEST "" "FOLDER" "" ${ARGN}) + if (NOT DEFINED USVFS_TEST_FOLDER) + set(folder "tests") + else() + set(folder "tests/${USVFS_TEST_FOLDER}") + endif() + set_target_properties(${TARGET} + PROPERTIES + FOLDER ${folder} + RUNTIME_OUTPUT_NAME ${TARGET}${ARCH_POSTFIX} + RUNTIME_OUTPUT_DIRECTORY_DEBUG ${USVFS_TEST_BINDIR} + RUNTIME_OUTPUT_DIRECTORY_RELEASE ${USVFS_TEST_BINDIR} + RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO ${USVFS_TEST_BINDIR} + ) +endfunction() + +#! usvfs_target_link_usvfs +# +# add a target link between the given target and the usvfs shared library with delay +# loading +# +function(usvfs_target_link_usvfs TARGET) + target_link_libraries(${TARGET} PRIVATE usvfs_dll) + target_link_options(${TARGET} PRIVATE "/DELAYLOAD:usvfs${ARCH_POSTFIX}.dll") +endfunction() + + +file(GLOB directories LIST_DIRECTORIES true "*") + +# this goes through all the directories and +# +# 1. add them if there is a CMakeLists.txt inside +# 2. add correspondings tests (for CTest) for BOTH x86 and x64 when possible +# +foreach(directory ${directories}) + if(NOT(IS_DIRECTORY ${directory})) + continue() + endif() + + if(NOT(EXISTS ${directory}/CMakeLists.txt)) + continue() + endif() + + add_subdirectory(${directory}) + + get_filename_component(dirname ${directory} NAME) + if((dirname STREQUAL "test_utils") OR (dirname STREQUAL "gtest_utils")) + continue() + endif() + + add_test(NAME ${dirname}_x64 + COMMAND ${USVFS_TEST_BINDIR}/${dirname}_x64.exe + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + ) + add_test(NAME ${dirname}_x86 + COMMAND ${USVFS_TEST_BINDIR}/${dirname}_x86.exe + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + ) +endforeach() diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/data/file.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/data/file.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/docs/doc.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/docs/doc.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/empty/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/empty/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/readme.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/readme.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/data/file.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/data/file.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/docs/doc.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/docs/doc.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/empty/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/empty/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/info.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/info.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/readme.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/readme.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/data/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/doc.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/doc.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/subdocs/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/subdocs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/data/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/doc.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/doc.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/subdocs/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/subdocs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/example.cpp b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/example.cpp new file mode 100644 index 0000000..9a79d16 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/example.cpp @@ -0,0 +1,139 @@ +#include +#include + +std::filesystem::path storages_path; +std::filesystem::path hudpainter_path; +bool has_mo2; + +// See below main() for implementation. +void request_directory(std::filesystem::path); +std::filesystem::path restrict_path(std::filesystem::path); +std::vector get_files(); +std::string read_file(std::filesystem::path); +void write_file(std::filesystem::path); + +// Expected path when not using MO2 +int main() +{ + // RedFileSystem plugin (DLL) is loaded. + has_mo2 = GetModuleHandle(TEXT("usvfs_x64.dll")) != nullptr; + + // Setup paths + auto path = std::filesystem::absolute("."); // \bin\x64 + auto game_path = path.parent_path().parent_path(); // + + storages_path = game_path / "r6" / "storages"; // \r6\storages + + // Create storages directory if it is no present. + request_directory(storages_path); // \r6\storages + + // Game is running... + + // HUD Painter request its storage endpoint. + hudpainter_path = storages_path / "HUDPainter"; + request_directory(hudpainter_path); // \r6\storages\HUDPainter + + // HUD Painter request a file (present in archive). + auto default_path = + restrict_path("DEFAULT.json"); // \r6\storages\HUDPainter\DEFAULT.json + + // HUD Painter read file in `default_path` with success. + auto data = read_file(default_path); // \r6\storages\HUDPainter\DEFAULT.json + + // HUD Painter request another file. + auto test_path = + restrict_path("TEST.json"); // \r6\storages\HUDPainter\TEST.json + + // (Bug A) HUD Painter write file in `test_path`. + write_file(test_path); // \r6\storages\HUDPainter\TEST.json + // file is created with workaround, not created otherwise. + + // (Bug B) HUD Painter request list of files. + // TEST.json is successfully created when using workaround. + auto files = get_files(); // [0] \r6\storages\HUDPainter\DEFAULT.json + + // files.size() == 1 + + // Expect: + // files.size() == 2 + // + // [0] \r6\storages\HUDPainter\DEFAULT.json + // [1] \r6\storages\HUDPainter\TEST.json +} + +// Create directory if it is no present +void request_directory(std::filesystem::path path) +{ + bool is_present = std::filesystem::exists(path); + + if (is_present) { + return; + } + std::filesystem::create_directory(path); +} + +// Resolve path for HUDPainter (security layer) +std::filesystem::path restrict_path(std::filesystem::path path) +{ + auto real_path = std::filesystem::weakly_canonical(hudpainter_path / path); + + // Workaround when using MO2 + if (has_mo2) { + return real_path; + } + // Expected implementation without MO2 + if (real_path.string().find(hudpainter_path.string() + "\\") != 0) { + // "throw" error + } + return real_path; +} + +// List files in storage of HUDPainter. +std::vector get_files() +{ + std::vector files; + auto entries = std::filesystem::directory_iterator(hudpainter_path); + + for (const auto& entry : entries) { + if (entry.is_regular_file()) { + auto file_name = entry.path().filename(); + auto file_path = + hudpainter_path / + file_name; // Culprit of bug B. When using entry.path() directly, it works. + + files.emplace_back(file_path); + } + } + return files; +} + +std::string read_file(std::filesystem::path path) +{ + std::ifstream stream; + + // With workaround: + // std::filesystem::create_directories(path.parent_path()); + stream.open(path); + if (!stream.is_open()) { + return ""; + } + std::stringstream data; + + data << stream.rdbuf(); + stream.close(); + return data.str(); +} + +void write_file(std::filesystem::path path) +{ + std::ofstream stream; + + // With workaround: + // std::filesystem::create_directories(path.parent_path()); + stream.open(path, std::ios_base::trunc); + if (!stream.is_open()) { + return; + } + stream << ""; + stream.close(); +} diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/data/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json new file mode 100644 index 0000000..dd85d0a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json @@ -0,0 +1,11 @@ +{ + "propertiesDefault": [ + { + "name": "MainColors.Red", + "red": 1.1761, + "green": 0.3809, + "blue": 0.3476, + "alpha": 1 + } + ] +} diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/overwrite/r6/storages/HUDPainter/TEST.json b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/overwrite/r6/storages/HUDPainter/TEST.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/overwrite/r6/storages/HUDPainter/TEST.json @@ -0,0 +1 @@ +{} diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/data/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json new file mode 100644 index 0000000..dd85d0a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json @@ -0,0 +1,11 @@ +{ + "propertiesDefault": [ + { + "name": "MainColors.Red", + "red": 1.1761, + "green": 0.3809, + "blue": 0.3476, + "alpha": 1 + } + ] +} diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/docs/doc.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/docs/doc.skip new file mode 100644 index 0000000..09a24fd --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/docs/doc.skip @@ -0,0 +1 @@ +doc.skip in data/docs diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/file.txt b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/file.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.skip new file mode 100644 index 0000000..4b8bde6 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.skip @@ -0,0 +1 @@ +doc.skip in mod1/docs diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.txt b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/empty/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/empty/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.skip new file mode 100644 index 0000000..1d1857a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.skip @@ -0,0 +1 @@ +readme.skip in mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.txt b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/overwrite/readme.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/overwrite/readme.skip new file mode 100644 index 0000000..0062dba --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/overwrite/readme.skip @@ -0,0 +1 @@ +readme.skip in overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/docs/doc.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/docs/doc.skip new file mode 100644 index 0000000..09a24fd --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/docs/doc.skip @@ -0,0 +1 @@ +doc.skip in data/docs diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/file.txt b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/file.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.skip new file mode 100644 index 0000000..4b8bde6 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.skip @@ -0,0 +1 @@ +doc.skip in mod1/docs diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.txt b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/empty/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/empty/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.skip new file mode 100644 index 0000000..1d1857a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.skip @@ -0,0 +1 @@ +readme.skip in mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.txt b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rcopyme4.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rcopyme4.txt new file mode 100644 index 0000000..d1e44e9 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rcopyme4.txt @@ -0,0 +1 @@ +rfolder\rcopyme4.txt original contents diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfile0.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfile0.txt new file mode 100644 index 0000000..9830306 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfile0.txt @@ -0,0 +1 @@ +rfolder\rfile0.txt original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfiledeletewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfiledeletewrite.txt new file mode 100644 index 0000000..2743678 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfiledeletewrite.txt @@ -0,0 +1 @@ +rfolder\rfiledeletewrite.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfilerewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfilerewrite.txt new file mode 100644 index 0000000..bdff476 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfilerewrite.txt @@ -0,0 +1 @@ +rfolder\rfilerewrite.txt rewrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0.txt new file mode 100644 index 0000000..a52b6be --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0.txt @@ -0,0 +1 @@ +root0 original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0w.txt new file mode 100644 index 0000000..c177584 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0w.txt @@ -0,0 +1 @@ +root0w original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1.txt new file mode 100644 index 0000000..06dc692 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1.txt @@ -0,0 +1 @@ +root1 original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1w.txt new file mode 100644 index 0000000..45172aa --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1w.txt @@ -0,0 +1 @@ +root1w original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rcopyme4.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rcopyme4.txt new file mode 100644 index 0000000..d1e44e9 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rcopyme4.txt @@ -0,0 +1 @@ +rfolder\rcopyme4.txt original contents diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfile0.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfile0.txt new file mode 100644 index 0000000..9830306 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfile0.txt @@ -0,0 +1 @@ +rfolder\rfile0.txt original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledelete.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledelete.txt new file mode 100644 index 0000000..c19828f --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledelete.txt @@ -0,0 +1 @@ +rfolder\rfiledelete.txt original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledeletewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledeletewrite.txt new file mode 100644 index 0000000..93861cc --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledeletewrite.txt @@ -0,0 +1 @@ +rfolder\rfiledeletewrite.txt orignal file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfileoldname.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfileoldname.txt new file mode 100644 index 0000000..9376ea1 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfileoldname.txt @@ -0,0 +1 @@ +rfolder\rfileoldname.txt original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfilerewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfilerewrite.txt new file mode 100644 index 0000000..499775d --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfilerewrite.txt @@ -0,0 +1 @@ +rfolder\rfilerewrite.txt original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0.txt new file mode 100644 index 0000000..a52b6be --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0.txt @@ -0,0 +1 @@ +root0 original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0w.txt new file mode 100644 index 0000000..c177584 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0w.txt @@ -0,0 +1 @@ +root0w original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1.txt new file mode 100644 index 0000000..06dc692 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1.txt @@ -0,0 +1 @@ +root1 original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1w.txt new file mode 100644 index 0000000..45172aa --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1w.txt @@ -0,0 +1 @@ +root1w original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfile.txt new file mode 100644 index 0000000..227256a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfile.txt @@ -0,0 +1 @@ +mfolder1\mfile.txt by mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfilew.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfilew.txt new file mode 100644 index 0000000..420589d --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfilew.txt @@ -0,0 +1 @@ +mfolder1\mfilew.txt by mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder3/mdeep3/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder3/mdeep3/mfile.txt new file mode 100644 index 0000000..6a7a0c2 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder3/mdeep3/mfile.txt @@ -0,0 +1 @@ +mfolder3\mdeep3\mfile.txt by mod1 (to be hidden by mod3) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1.txt new file mode 100644 index 0000000..7d5dc0e --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1.txt @@ -0,0 +1 @@ +hello mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1w.txt new file mode 100644 index 0000000..1838cdf --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1w.txt @@ -0,0 +1 @@ +hello mod1w diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder2/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder2/mfile.txt new file mode 100644 index 0000000..8ff096e --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder2/mfile.txt @@ -0,0 +1 @@ +mfolder2\mfile.txt by mod2 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfile.txt new file mode 100644 index 0000000..c0f0f2c --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfile.txt @@ -0,0 +1 @@ +mfolder4\mfile.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove.txt new file mode 100644 index 0000000..e35f1f3 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletemove.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove2p.txt new file mode 100644 index 0000000..cc572dc --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove2p.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletemove2p.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite.txt new file mode 100644 index 0000000..f699360 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletewrite.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite2p.txt new file mode 100644 index 0000000..9eae9c5 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite2p.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletewrite2p.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilemoveover.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilemoveover.txt new file mode 100644 index 0000000..88a6d08 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilemoveover.txt @@ -0,0 +1 @@ +mfolder4/mfilemoveover.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfileoverwrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfileoverwrite.txt new file mode 100644 index 0000000..61b92a6 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfileoverwrite.txt @@ -0,0 +1 @@ +mfolder4\mfileoverwrite.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilerewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilerewrite.txt new file mode 100644 index 0000000..727a48f --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilerewrite.txt @@ -0,0 +1 @@ +mfolder4\mfilerewrite.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mod2.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mod2.txt new file mode 100644 index 0000000..49bbbf1 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mod2.txt @@ -0,0 +1 @@ +hello mod2 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder3/mdeep3/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder3/mdeep3/mfile.txt new file mode 100644 index 0000000..fac8213 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder3/mdeep3/mfile.txt @@ -0,0 +1 @@ +mfolder3\mdeep3\mfile.txt by mod3 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder4/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder4/mfile.txt new file mode 100644 index 0000000..5e6ca00 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder4/mfile.txt @@ -0,0 +1 @@ +mfolder4\mfile.txt by mod3 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mod3.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mod3.txt new file mode 100644 index 0000000..47fbacd --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mod3.txt @@ -0,0 +1 @@ +hello mod3 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfile.txt new file mode 100644 index 0000000..c67ecfe --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfile.txt @@ -0,0 +1 @@ +mfolder4\mfile.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletemove.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletemove.txt new file mode 100644 index 0000000..00cc0cd --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletemove.txt @@ -0,0 +1 @@ +mfolder4\mfiledeletemove.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletewrite.txt new file mode 100644 index 0000000..083a6b8 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletewrite.txt @@ -0,0 +1 @@ +mfolder4\mfiledeletewrite.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilemoveover.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilemoveover.txt new file mode 100644 index 0000000..a8a93c2 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilemoveover.txt @@ -0,0 +1 @@ +mfolder4\mfilemoveover.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfileoverwrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfileoverwrite.txt new file mode 100644 index 0000000..fc6d42f --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfileoverwrite.txt @@ -0,0 +1 @@ +mfolder4\mfileoverwrite.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilerewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilerewrite.txt new file mode 100644 index 0000000..a3b8ced --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilerewrite.txt @@ -0,0 +1 @@ +mfolder4\mfilerewrite.txt rewrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/.empty b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/.empty new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfile1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfile1.txt new file mode 100644 index 0000000..bf76f1d --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfile1.txt @@ -0,0 +1 @@ +newfile1.txt nonrecursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfolder1/newfile1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfolder1/newfile1.txt new file mode 100644 index 0000000..4802577 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfolder1/newfile1.txt @@ -0,0 +1 @@ +newfile1.txt nonrecursive overwrite subfolder diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder2/newfile2.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder2/newfile2.txt new file mode 100644 index 0000000..a4fa7fc --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder2/newfile2.txt @@ -0,0 +1 @@ +newfile2.txt recursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3.txt new file mode 100644 index 0000000..933eb0c --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3.txt @@ -0,0 +1 @@ +newfile3.txt recursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3e.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3e.txt new file mode 100644 index 0000000..5e5286a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3e.txt @@ -0,0 +1 @@ +newfile3e.txt recursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletemove2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletemove2p.txt new file mode 100644 index 0000000..6b85a92 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletemove2p.txt @@ -0,0 +1 @@ +mfolder4\mfiledeletemove2p.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletewrite2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletewrite2p.txt new file mode 100644 index 0000000..64cac84 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletewrite2p.txt @@ -0,0 +1 @@ +mfolder4\mfiledeletewrite2p.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4.txt new file mode 100644 index 0000000..cba5361 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4.txt @@ -0,0 +1 @@ +newfile4.txt recursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4e.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4e.txt new file mode 100644 index 0000000..eb90089 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4e.txt @@ -0,0 +1 @@ +newfile4e.txt recursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4enr.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4enr.txt new file mode 100644 index 0000000..f8b5f9f --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4enr.txt @@ -0,0 +1 @@ +newfile4enr.txt nonrecursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4.txt new file mode 100644 index 0000000..06edab8 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4.txt @@ -0,0 +1 @@ +epnewfile4.txt nonrecursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4r.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4r.txt new file mode 100644 index 0000000..8fd183d --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4r.txt @@ -0,0 +1 @@ +epnewfile4r.txt recursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4p/rcopyme4.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4p/rcopyme4.txt new file mode 100644 index 0000000..d1e44e9 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4p/rcopyme4.txt @@ -0,0 +1 @@ +rfolder\rcopyme4.txt original contents diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/rcopyme4.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/rcopyme4.txt new file mode 100644 index 0000000..d1e44e9 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/rcopyme4.txt @@ -0,0 +1 @@ +rfolder\rcopyme4.txt original contents diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/rfolder/rfilenewname.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/rfolder/rfilenewname.txt new file mode 100644 index 0000000..9376ea1 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/rfolder/rfilenewname.txt @@ -0,0 +1 @@ +rfolder\rfileoldname.txt original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1.txt new file mode 100644 index 0000000..5aa129b --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1.txt @@ -0,0 +1 @@ +root1 source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1w.txt new file mode 100644 index 0000000..0c04aa5 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1w.txt @@ -0,0 +1 @@ +root1w source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2.txt new file mode 100644 index 0000000..4ac40bb --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2.txt @@ -0,0 +1 @@ +root2 source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2w.txt new file mode 100644 index 0000000..83ddb42 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2w.txt @@ -0,0 +1 @@ +root2w source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfile.txt new file mode 100644 index 0000000..227256a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfile.txt @@ -0,0 +1 @@ +mfolder1\mfile.txt by mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfilew.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfilew.txt new file mode 100644 index 0000000..420589d --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfilew.txt @@ -0,0 +1 @@ +mfolder1\mfilew.txt by mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder3/mdeep3/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder3/mdeep3/mfile.txt new file mode 100644 index 0000000..6a7a0c2 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder3/mdeep3/mfile.txt @@ -0,0 +1 @@ +mfolder3\mdeep3\mfile.txt by mod1 (to be hidden by mod3) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1.txt new file mode 100644 index 0000000..7d5dc0e --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1.txt @@ -0,0 +1 @@ +hello mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1w.txt new file mode 100644 index 0000000..1838cdf --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1w.txt @@ -0,0 +1 @@ +hello mod1w diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder2/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder2/mfile.txt new file mode 100644 index 0000000..8ff096e --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder2/mfile.txt @@ -0,0 +1 @@ +mfolder2\mfile.txt by mod2 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfile.txt new file mode 100644 index 0000000..c0f0f2c --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfile.txt @@ -0,0 +1 @@ +mfolder4\mfile.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove.txt new file mode 100644 index 0000000..e35f1f3 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletemove.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove2p.txt new file mode 100644 index 0000000..cc572dc --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove2p.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletemove2p.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite.txt new file mode 100644 index 0000000..f699360 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletewrite.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite2p.txt new file mode 100644 index 0000000..9eae9c5 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite2p.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletewrite2p.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilemoveover.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilemoveover.txt new file mode 100644 index 0000000..88a6d08 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilemoveover.txt @@ -0,0 +1 @@ +mfolder4/mfilemoveover.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfileoverwrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfileoverwrite.txt new file mode 100644 index 0000000..61b92a6 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfileoverwrite.txt @@ -0,0 +1 @@ +mfolder4\mfileoverwrite.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilerewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilerewrite.txt new file mode 100644 index 0000000..727a48f --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilerewrite.txt @@ -0,0 +1 @@ +mfolder4\mfilerewrite.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mod2.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mod2.txt new file mode 100644 index 0000000..49bbbf1 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mod2.txt @@ -0,0 +1 @@ +hello mod2 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder3/mdeep3/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder3/mdeep3/mfile.txt new file mode 100644 index 0000000..fac8213 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder3/mdeep3/mfile.txt @@ -0,0 +1 @@ +mfolder3\mdeep3\mfile.txt by mod3 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder4/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder4/mfile.txt new file mode 100644 index 0000000..5e6ca00 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder4/mfile.txt @@ -0,0 +1 @@ +mfolder4\mfile.txt by mod3 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mod3.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mod3.txt new file mode 100644 index 0000000..47fbacd --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mod3.txt @@ -0,0 +1 @@ +hello mod3 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfile.txt new file mode 100644 index 0000000..c67ecfe --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfile.txt @@ -0,0 +1 @@ +mfolder4\mfile.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove.txt new file mode 100644 index 0000000..657f07d --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletemove.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove2p.txt new file mode 100644 index 0000000..cfd08dd --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove2p.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletemove2p.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite.txt new file mode 100644 index 0000000..a17d129 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletewrite.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite2p.txt new file mode 100644 index 0000000..b198d1c --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite2p.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletewrite2p.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilemoveover.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilemoveover.txt new file mode 100644 index 0000000..b395908 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilemoveover.txt @@ -0,0 +1 @@ +mfolder4/mfilemoveover.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfileoverwrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfileoverwrite.txt new file mode 100644 index 0000000..405c0c1 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfileoverwrite.txt @@ -0,0 +1 @@ +mfolder4\mfileoverwrite.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilerewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilerewrite.txt new file mode 100644 index 0000000..def973c --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilerewrite.txt @@ -0,0 +1 @@ +mfolder4\mfilerewrite.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/overwrite/.empty b/libs/usvfs/test/fixtures/usvfs_test/basic/source/overwrite/.empty new file mode 100644 index 0000000..e69de29 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/root1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root1.txt new file mode 100644 index 0000000..5aa129b --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root1.txt @@ -0,0 +1 @@ +root1 source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/root1w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root1w.txt new file mode 100644 index 0000000..0c04aa5 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root1w.txt @@ -0,0 +1 @@ +root1w source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/root2.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root2.txt new file mode 100644 index 0000000..4ac40bb --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root2.txt @@ -0,0 +1 @@ +root2 source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/root2w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root2w.txt new file mode 100644 index 0000000..83ddb42 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root2w.txt @@ -0,0 +1 @@ +root2w source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/vfs_mappings.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/vfs_mappings.txt new file mode 100644 index 0000000..cc755b4 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/vfs_mappings.txt @@ -0,0 +1,26 @@ +# mapdir +# +# ... +mapdir + mod1 + mod2 + mod3 + mod4 + +# mapdircreate +# +# ... +mapdircreate + overwrite + +# mapfile +# +# ... +mapfile root1.txt + root1.txt +mapfile root1w.txt + root1w.txt +mapfile root2.txt + root2.txt +mapfile root2w.txt + root2w.txt diff --git a/libs/usvfs/test/gtest_utils/CMakeLists.txt b/libs/usvfs/test/gtest_utils/CMakeLists.txt new file mode 100644 index 0000000..8dcda70 --- /dev/null +++ b/libs/usvfs/test/gtest_utils/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) + +add_library(gtest_utils STATIC + gtest_utils.cpp + gtest_utils.h +) +set_target_properties(gtest_utils PROPERTIES FOLDER tests) +target_link_libraries(gtest_utils PUBLIC GTest::gtest) +target_include_directories(gtest_utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/libs/usvfs/test/gtest_utils/gtest_utils.cpp b/libs/usvfs/test/gtest_utils/gtest_utils.cpp new file mode 100644 index 0000000..f292dfd --- /dev/null +++ b/libs/usvfs/test/gtest_utils/gtest_utils.cpp @@ -0,0 +1,122 @@ +#include "gtest_utils.h" + +// this file is shared by both usvfs_global_test and usvfs_global_test_runner +// + +#include +#include +#include + +#include + +#include + +std::string trimmed(std::string content) +{ + boost::algorithm::trim(content); + return content; +} + +// small utility to read files +std::optional read_content(const std::filesystem::path& path, + bool trim = true) +{ + std::ifstream ifs(path, std::ios::binary | std::ios::ate); + + if (!ifs) { + return {}; + } + + const auto count = ifs.tellg(); + + std::string buffer(static_cast(count), '\0'); + + ifs.seekg(0, std::ios::beg); + ifs.read(buffer.data(), count); + + if (trim) { + boost::algorithm::trim(buffer); + } + + return buffer; +} + +::testing::AssertionResult AssertDirectoryEquals(const std::filesystem::path& expected, + const std::filesystem::path& actual, + bool content) +{ + std::vector failure_messages; + std::vector in_both; + + // iterate left, check on right + for (const auto& it : std::filesystem::recursive_directory_iterator{expected}) { + const auto relpath = relative(it.path(), expected); + if (!exists(actual / relpath)) { + failure_messages.push_back( + std::format("{} expected but not found", relpath.string())); + } else { + in_both.push_back(relpath); + } + } + + // iterate right, check on left + for (const auto& it : std::filesystem::recursive_directory_iterator{actual}) { + const auto relpath = relative(it.path(), actual); + if (!exists(expected / relpath)) { + failure_messages.push_back( + std::format("{} found but not expected", relpath.string())); + } + } + + // check contents + if (content) { + for (const auto& relpath : in_both) { + const auto expected_path = expected / relpath, actual_path = actual / relpath; + + if (is_directory(expected_path) != is_directory(actual_path)) { + failure_messages.push_back( + std::format("{} type mismatch, expected {} but found {}", relpath.string(), + is_directory(expected_path) ? "directory" : "file", + is_directory(expected_path) ? "file" : "directory")); + continue; + } + + if (is_directory(expected_path)) { + continue; + } + + if (read_content(expected_path) != read_content(actual_path)) { + failure_messages.push_back( + std::format("{} content mismatch", relpath.string())); + } + } + } + + if (failure_messages.empty()) { + return ::testing::AssertionSuccess(); + } + + return ::testing::AssertionFailure() + << "\n" + << boost::algorithm::join(failure_messages, "\n") << "\n"; +} + +::testing::AssertionResult AssertContentEquals(std::string_view expected, + const std::filesystem::path& path, + bool trim) +{ + const auto content = read_content(path, trim); + + if (!content) { + return ::testing::AssertionFailure() + << "failed to open path '" << path.string() << "'"; + } + + if (*content != expected) { + return ::testing::AssertionFailure() + << "mismatch content for '" << path.string() << "', expected '" << expected + << "', found '" << *content << "'"; + } + + return ::testing::AssertionSuccess(); +} diff --git a/libs/usvfs/test/gtest_utils/gtest_utils.h b/libs/usvfs/test/gtest_utils/gtest_utils.h new file mode 100644 index 0000000..1dba70c --- /dev/null +++ b/libs/usvfs/test/gtest_utils/gtest_utils.h @@ -0,0 +1,29 @@ +#pragma once + +// this file is shared by both usvfs_global_test and usvfs_global_test_runner +// + +#include + +#include + +::testing::AssertionResult AssertDirectoryEquals(const std::filesystem::path& expected, + const std::filesystem::path& actual, + bool content = true); + +::testing::AssertionResult AssertContentEquals(std::string_view expected, + const std::filesystem::path& path, + bool trim = true); + +// macro to assert that the contents of two directories are identical - directories are +// compared recursively and file contents are compared (excluding extra spaces or lines +// in file) +// +#define ASSERT_DIRECTORY_EQ(Expected, Actual) \ + ASSERT_TRUE(AssertDirectoryEquals(Expected, Actual)) + +// macro to assert that the contents of two files are identical (excluding extra space +// or lines in file) +// +#define ASSERT_CONTENT_EQ(Expected, Path) \ + ASSERT_TRUE(AssertContentEquals(Expected, Path)) diff --git a/libs/usvfs/test/shared_test/CMakeLists.txt b/libs/usvfs/test/shared_test/CMakeLists.txt new file mode 100644 index 0000000..c5a42ea --- /dev/null +++ b/libs/usvfs/test/shared_test/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) + +add_executable(shared_test main.cpp) +usvfs_set_test_properties(shared_test) +target_link_libraries(shared_test PRIVATE test_utils GTest::gtest GTest::gtest_main) diff --git a/libs/usvfs/test/shared_test/main.cpp b/libs/usvfs/test/shared_test/main.cpp new file mode 100644 index 0000000..c6b5be6 --- /dev/null +++ b/libs/usvfs/test/shared_test/main.cpp @@ -0,0 +1,248 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#define PRIVATE public +#include +#undef PRIVATE + +#include + +using namespace usvfs::shared; + +using namespace boost::interprocess; + +static const char g_SHMName[] = "treetest_shm"; +static const char TreeName[] = "treetest_tree"; + +typedef DirectoryTree TreeType; +typedef TreeContainer ContainerType; + +typedef boost::container::scoped_allocator_adaptor< + boost::interprocess::allocator> + VoidAllocator; +typedef VoidAllocator::rebind::other CharAllocator; +typedef basic_string, CharAllocator> SHMString; +typedef DirectoryTree ComplexTreeType; +typedef TreeContainer ComplexContainerType; + +template <> +struct usvfs::shared::SHMDataCreator +{ + static int create(int source, const VoidAllocatorT&) { return source; } +}; + +template <> +inline int usvfs::shared::createDataEmpty(const typename VoidAllocatorT&) +{ + return 0; +} + +template <> +inline void usvfs::shared::dataAssign(int& destination, const int& source) +{ + destination = source; +} + +template <> +inline SHMString +usvfs::shared::createDataEmpty(const typename VoidAllocatorT& allocator) +{ + return SHMString("", allocator); +} + +template <> +inline void usvfs::shared::dataAssign(SHMString& destination, + const SHMString& source) +{ + destination.assign(source.c_str()); +} + +static std::shared_ptr logger() +{ + std::shared_ptr result = spdlog::get("test"); + if (result.get() == nullptr) { + result = spdlog::stdout_logger_mt("test"); + } + return result; +} + +TEST(WildcardTest, MatchWildcard) +{ + EXPECT_TRUE(wildcard::Match(TEXT("abc"), TEXT("a*"))); + EXPECT_TRUE(wildcard::Match(TEXT("abc"), TEXT("a*c"))); + EXPECT_TRUE(wildcard::Match(TEXT("abc"), TEXT("a?c"))); + EXPECT_TRUE(wildcard::Match(TEXT("abc"), TEXT("abc"))); + EXPECT_TRUE(wildcard::Match(TEXT("abc"), TEXT("abc*"))); + + EXPECT_TRUE(wildcard::Match("abc", "*.*")); + EXPECT_TRUE(wildcard::Match("abc.def", "*")); + EXPECT_TRUE(wildcard::Match(TEXT("abc"), TEXT("*.*"))); + EXPECT_TRUE(wildcard::Match(TEXT("abc.def"), TEXT("*"))); + + EXPECT_NE(nullptr, wildcard::PartialMatch("abc", "*.*")); + EXPECT_NE(nullptr, wildcard::PartialMatch("abc.def", "*")); + EXPECT_EQ('\0', *wildcard::PartialMatch("abc", "*.*")); + EXPECT_EQ('\0', *wildcard::PartialMatch("abc.def", "*")); + + EXPECT_FALSE(wildcard::Match(TEXT("abc"), TEXT("b*"))); +} + +TEST(DirectoryTreeTest, SimpleTreeInit) +{ + EXPECT_NO_THROW({ + ContainerType tree(g_SHMName, 4096); + TreeType::NodePtrT p = tree.addFile(R"(C:\temp\test.txt)", 42, false); + EXPECT_NE(TreeType::NodePtrT(), p); + }); +} + +TEST(DirectoryTreeTest, FindNode) +{ + shared_memory_object::remove(g_SHMName); + ContainerType tree(g_SHMName, 64 * 1024); + EXPECT_NE(nullptr, tree.addFile(R"(C:\temp\bla)", 0x42, 0, false)); + + EXPECT_NE(nullptr, tree->findNode(R"(C:\temp)").get()); + EXPECT_EQ(nullptr, tree->findNode(R"(C:\temp\bla\blubb)").get()); +} + +struct TestVisitor +{ + TreeType::NodePtrT lastNode; + bool flag40{false}; + + void operator()(const TreeType::NodePtrT& node) + { + lastNode = node; + flag40 = node->hasFlag(0x40); + logger()->debug("{0} - {1}", lastNode->name(), flag40); + // BOOST_LOG_SEV(globalLogger::get(), LogLevel::Debug) << lastNode->name() << " - " + // << flag40; + } +}; + +TEST(DirectoryTreeTest, VisitPath) +{ + shared_memory_object::remove(g_SHMName); + ContainerType tree(g_SHMName, 64 * 1024); + EXPECT_NE(nullptr, tree.addFile(R"(C:\temp\bla)", 1, 0x40, false)); + + TestVisitor visitor; + + tree->visitPath(R"(C:\temp\bla\blubb)", + TreeType::VisitorFunction([&](const TreeType::NodePtrT& node) { + visitor(node); + })); + EXPECT_TRUE(visitor.flag40); + EXPECT_EQ("bla", visitor.lastNode->name()); +} + +TEST(DirectoryTreeTest, WildCardFind) +{ + shared_memory_object::remove(g_SHMName); + EXPECT_NO_THROW({ + ContainerType tree(g_SHMName, 64 * 1024); + + EXPECT_NE(nullptr, tree.addFile(R"(C:\temp)", 1, FLAG_DIRECTORY, false)); + EXPECT_NE(nullptr, tree.addFile(R"(C:\temp\abc)", 1, 0, false)); + EXPECT_NE(nullptr, tree.addFile(R"(C:\temp\abd)", 2, 0, false)); + EXPECT_NE(nullptr, tree.addFile(R"(C:\temp\ace)", 3, 0, false)); + + EXPECT_EQ(3, tree->find(R"(C:\temp\*)").size()); + EXPECT_NE(nullptr, tree->node("C:")); + EXPECT_EQ(1, tree->node("C:")->find("*").size()); + EXPECT_NE(nullptr, tree->node("C:")->node("temp")); + EXPECT_EQ(3, tree->node("C:")->node("temp")->find("*").size()); // alternative + // search on the top-level + EXPECT_EQ(1, tree->find("*").size()); + // * should work + EXPECT_EQ(2, tree->find(R"(C:\temp\ab*)").size()); + // * does not match directory separators + EXPECT_EQ(0, tree->find("*ab*").size()); + // matches only the directory itself + EXPECT_EQ(1, tree->find(R"(C:\temp*)").size()); + }); +} + +TEST(DirectoryTreeTest, SHMAllocation) +{ + EXPECT_NO_THROW({ + ContainerType create(g_SHMName, 64 * 1024); + + { // creation + create.addFile(R"(C:\temp\abc)", 1, false); + create.addFile(R"(C:\temp\abd)", 2, false); + create.addFile(R"(C:\temp\ace)", 3, false); + } + + { // access + ContainerType access(g_SHMName, 64 * 1024); + EXPECT_NE(nullptr, access.get()); + std::vector res = access->find(R"(C:\temp\*)"); + EXPECT_EQ(3, res.size()); // matches the three files + EXPECT_EQ(access->m_Self.lock().get(), access->node("C:")->parent().get()); + } + }); +} + +TEST(DirectoryTreeTest, SHMAllocationError) +{ + EXPECT_NO_THROW({ + try { + ContainerType tree(g_SHMName, 4096); + int c = 0; + for (char i = 'a'; i <= 'z'; ++i) { + for (char j = 'a'; j <= 'z'; ++j) { + std::string name = std::string(R"(C:\temp\)") + i + j; + tree.addFile(name, ++c, false); + } + } + + EXPECT_EQ(1, tree->node("C:")->node("temp")->node("aa", MissingThrow)->data()); + EXPECT_EQ(26, tree->node("C:")->node("temp")->node("az", MissingThrow)->data()); + } catch (const std::exception& e) { + logger()->error("{0}", e.what()); + // BOOST_LOG_SEV(globalLogger::get(), LogLevel::Error) << e.what(); + throw; + } + }); +} + +TEST(DirectoryTreeTest, SHMAllocationErrorComplex) +{ + EXPECT_NO_THROW({ + try { + ComplexContainerType tree(g_SHMName, 4096); + SHMString str = tree.create("gaga"); + for (char i = 'a'; i <= 'z'; ++i) { + for (char j = 'a'; j <= 'z'; ++j) { + std::string name = std::string(R"(C:\temp\)") + i + j; + tree.addFile(name, str, false); + } + } + EXPECT_STREQ( + str.c_str(), + tree->node("C:")->node("temp")->node("aa", MissingThrow)->data().c_str()); + EXPECT_STREQ( + str.c_str(), + tree->node("C:")->node("temp")->node("az", MissingThrow)->data().c_str()); + } catch (const std::exception& e) { + logger()->error("{}", e.what()); + throw; + } + }); +} + +int main(int argc, char** argv) +{ + auto logger = spdlog::stdout_logger_mt("usvfs"); + logger->set_level(spdlog::level::warn); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/libs/usvfs/test/test_utils/CMakeLists.txt b/libs/usvfs/test/test_utils/CMakeLists.txt new file mode 100644 index 0000000..88c75d6 --- /dev/null +++ b/libs/usvfs/test/test_utils/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) + +add_library(test_utils STATIC + test_helpers.cpp + test_helpers.h +) +set_target_properties(test_utils PROPERTIES FOLDER tests) +target_link_libraries(test_utils PUBLIC shared) +target_include_directories(test_utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/libs/usvfs/test/test_utils/test_helpers.cpp b/libs/usvfs/test/test_utils/test_helpers.cpp new file mode 100644 index 0000000..2daf3bb --- /dev/null +++ b/libs/usvfs/test/test_utils/test_helpers.cpp @@ -0,0 +1,248 @@ +#pragma once + +#include + +#include "test_helpers.h" +#include "winapi.h" + +namespace test +{ + +std::string FuncFailed::msg(std::string_view func, const char* arg1, + const unsigned long* res, const char* what) +{ + std::string buffer; + buffer.reserve(128); + + std::format_to(std::back_inserter(buffer), "{}() {}", func, what ? what : "failed"); + const char* sep = " : "; + if (arg1) { + std::format_to(std::back_inserter(buffer), "{}{}", sep, arg1); + sep = ", "; + } + if (res) { + std::format_to(std::back_inserter(buffer), "{}result = {} ({:#x})", sep, *res, + *res); + } + return buffer; +} + +ScopedFILE ScopedFILE::open(const std::filesystem::path& filepath, + std::wstring_view mode, errno_t& err) +{ + FILE* fp = nullptr; + err = _wfopen_s(&fp, filepath.c_str(), mode.data()); + if (err || !fp) { + return ScopedFILE(); + } + return ScopedFILE(fp); +} + +ScopedFILE ScopedFILE::open(const std::filesystem::path& filepath, + std::wstring_view mode) +{ + errno_t err; + auto file = open(filepath, mode, err); + if (err || !file) { + throw_testWinFuncFailed("_wfopen_s", filepath.string().c_str(), err); + } + return file; +} + +path path_of_test_bin(const path& relative) +{ + path base(winapi::wide::getModuleFileName(nullptr)); + return relative.empty() ? base.parent_path() : base.parent_path() / relative; +} + +path path_of_test_temp(const path& relative) +{ + return path_of_test_bin().parent_path() / "temp" / relative; +} + +path path_of_test_fixtures(const path& relative) +{ + return path_of_test_bin().parent_path() / "fixtures" / relative; +} + +path path_of_usvfs_lib(const path& relative) +{ + return path_of_test_bin().parent_path().parent_path() / "lib" / relative; +} + +std::string platform_dependant_executable(const char* name, const char* ext, + const char* platform) +{ + std::string res = name; + res += "_"; + if (platform) + res += platform; + else +#if _WIN64 + res += "x64"; +#else + res += "x86"; +#endif + res += "."; + res += ext; + return res; +} + +path path_as_relative(const path& base, const path& full_path) +{ + auto rel_begin = full_path.begin(); + auto base_iter = base.begin(); + while (rel_begin != full_path.end() && base_iter != base.end() && + *rel_begin == *base_iter) { + ++rel_begin; + ++base_iter; + } + + if (base_iter != base.end()) // full_path is not a sub-folder of base + return full_path; + + if (rel_begin == full_path.end()) // full_path == base + return path(L"."); + + // full_path is a sub-folder of base so take only relative path + path result; + for (; rel_begin != full_path.end(); ++rel_begin) + result /= *rel_begin; + return result; +} + +std::vector read_small_file(const path& file, bool binary) +{ + using namespace std; + + const auto f = ScopedFILE::open(file, binary ? L"rb" : L"rt"); + + if (fseek(f, 0, SEEK_END)) + throw_testWinFuncFailed("fseek", (unsigned long)0); + + long size = ftell(f); + if (size < 0) + throw_testWinFuncFailed("ftell", (unsigned long)size); + if (size > 0x10000000) // sanity check limit to 256M + throw test::FuncFailed("read_small_file", "file size too large", + (unsigned long)size); + + if (fseek(f, 0, SEEK_SET)) + throw_testWinFuncFailed("fseek", (unsigned long)0); + + std::vector content(static_cast(size)); + content.resize(fread(content.data(), sizeof(char), content.size(), f)); + + return content; +} + +bool compare_files(const path& file1, const path& file2, bool binary) +{ + // TODO: if this is ever used for big file should read files in chunks + return read_small_file(file1, binary) == read_small_file(file2, binary); +} + +bool is_empty_folder(const path& dpath, bool or_doesnt_exist) +{ + bool isDir = false; + if (!winapi::ex::wide::fileExists(dpath.c_str(), &isDir)) + return or_doesnt_exist; + + if (!isDir) + return false; + + for (const auto& f : winapi::ex::wide::quickFindFiles(dpath.c_str(), L"*")) + if (f.fileName != L"." && f.fileName != L"..") + return false; + return true; +} + +void delete_file(const path& file) +{ + if (!DeleteFileW(file.c_str())) { + auto err = GetLastError(); + if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND) + throw_testWinFuncFailed("DeleteFile", file.string()); + } +} + +void recursive_delete_files(const path& dpath) +{ + bool isDir = false; + if (!winapi::ex::wide::fileExists(dpath.c_str(), &isDir)) + return; + if (!isDir) + delete_file(dpath); + else { + // dpath exists and its a directory: + std::vector recurse; + for (const auto& f : winapi::ex::wide::quickFindFiles(dpath.c_str(), L"*")) { + if (f.fileName == L"." || f.fileName == L"..") + continue; + if (f.attributes & FILE_ATTRIBUTE_DIRECTORY) + recurse.push_back(f.fileName); + else + delete_file(dpath / f.fileName); + } + for (auto r : recurse) + recursive_delete_files(dpath / r); + if (!RemoveDirectoryW(dpath.c_str())) + throw_testWinFuncFailed("RemoveDirectory", dpath.string().c_str()); + } + if (winapi::ex::wide::fileExists(dpath.c_str())) + throw FuncFailed("delete_directory_tree", dpath.string().c_str()); +} + +void recursive_copy_files(const path& src_path, const path& dest_path, bool overwrite) +{ + bool srcIsDir = false, destIsDir = false; + if (!winapi::ex::wide::fileExists(src_path.c_str(), &srcIsDir)) + throw FuncFailed("recursive_copy", "source doesn't exist", + src_path.string().c_str()); + if (!winapi::ex::wide::fileExists(dest_path.c_str(), &destIsDir) && srcIsDir) { + winapi::ex::wide::createPath(dest_path.c_str()); + destIsDir = true; + } + + if (!srcIsDir) + if (!destIsDir) { + if (!CopyFileW(src_path.c_str(), dest_path.c_str(), overwrite)) + throw_testWinFuncFailed( + "CopyFile", (src_path.string() + " => " + dest_path.string()).c_str()); + return; + } else + throw FuncFailed("recursive_copy", + "source is a file but destination is a directory", + (src_path.string() + ", " + dest_path.string()).c_str()); + + if (!destIsDir) + throw FuncFailed("recursive_copy", + "source is a directory but destination is a file", + (src_path.string() + ", " + dest_path.string()).c_str()); + + // source and destination are both directories: + std::vector recurse; + for (const auto& f : winapi::ex::wide::quickFindFiles(src_path.c_str(), L"*")) { + if (f.fileName == L"." || f.fileName == L"..") + continue; + if (f.attributes & FILE_ATTRIBUTE_DIRECTORY) + recurse.push_back(f.fileName); + else if (!CopyFileW((src_path / f.fileName).c_str(), + (dest_path / f.fileName).c_str(), overwrite)) + throw_testWinFuncFailed("CopyFile", ((src_path / f.fileName).string() + " => " + + (dest_path / f.fileName).string())); + } + for (auto r : recurse) + recursive_copy_files(src_path / r, dest_path / r, overwrite); +} + +ScopedLoadLibrary::ScopedLoadLibrary(const wchar_t* dll_path) + : m_mod(LoadLibrary(dll_path)) +{} +ScopedLoadLibrary::~ScopedLoadLibrary() +{ + if (m_mod) + FreeLibrary(m_mod); +} + +}; // namespace test diff --git a/libs/usvfs/test/test_utils/test_helpers.h b/libs/usvfs/test/test_utils/test_helpers.h new file mode 100644 index 0000000..7ea8950 --- /dev/null +++ b/libs/usvfs/test/test_utils/test_helpers.h @@ -0,0 +1,165 @@ +#pragma once + +#include "windows_sane.h" + +#include +#include +#include + +namespace test +{ + +class FuncFailed : public std::runtime_error +{ +public: + FuncFailed(const char* func) : std::runtime_error(msg(func)) {} + FuncFailed(const char* func, unsigned long res) + : std::runtime_error(msg(func, nullptr, &res)) + {} + FuncFailed(const char* func, const char* arg1) : std::runtime_error(msg(func, arg1)) + {} + FuncFailed(const char* func, const char* arg1, unsigned long res) + : std::runtime_error(msg(func, arg1, &res)) + {} + FuncFailed(const char* func, const char* what, const char* arg1) + : std::runtime_error(msg(func, arg1, nullptr, what)) + {} + FuncFailed(const char* func, const char* what, const char* arg1, unsigned long res) + : std::runtime_error(msg(func, arg1, &res, what)) + {} + +private: + std::string msg(std::string_view func, const char* arg1 = nullptr, + const unsigned long* res = nullptr, const char* what = nullptr); +}; + +class WinFuncFailed : public std::runtime_error +{ +public: + using runtime_error::runtime_error; +}; + +class WinFuncFailedGenerator +{ +public: + WinFuncFailedGenerator(DWORD gle = GetLastError()) : m_gle(gle) {} + WinFuncFailedGenerator(const WinFuncFailedGenerator&) = delete; + + DWORD lastError() const { return m_gle; } + + WinFuncFailed operator()(std::basic_string_view func) + { + return WinFuncFailed(std::format("{} failed : lastError={}", func, m_gle)); + } + + WinFuncFailed operator()(std::basic_string_view func, unsigned long res) + { + return WinFuncFailed( + std::format("{} failed : result=({:#x}), lastError={}", func, res, m_gle)); + } + + WinFuncFailed operator()(std::string_view func, std::basic_string_view arg1) + { + return WinFuncFailed( + std::format("{} failed : {}, lastError={}", func, arg1, m_gle)); + } + + WinFuncFailed operator()(std::string_view func, std::basic_string_view arg1, + unsigned long res) + { + return WinFuncFailed(std::format("{} failed : {}, result=({:#x}), lastError={}", + func, arg1, res, m_gle)); + } + +private: + DWORD m_gle; +}; + +// trick to guarantee the evalutation of GetLastError() before the evalution of the +// parameters to the WinFuncFailed message generation +template +[[noreturn]] void throw_testWinFuncFailed(std::string_view func, Args&&... args) +{ + ::test::WinFuncFailedGenerator exceptionGenerator; + throw exceptionGenerator(func, std::forward(args)...); +} + +class ScopedFILE +{ +public: + // try to open the given filepath with the given mode, if it fails, set err to + // the return code of _wfopen_s and return a nulled scoped file + // + static ScopedFILE open(const std::filesystem::path& filepath, std::wstring_view mode, + errno_t& err); + + // same as above but throw a WinFuncFailed() exception if opening the file failed + // + static ScopedFILE open(const std::filesystem::path& filepath, std::wstring_view mode); + +public: + ScopedFILE(FILE* f = nullptr) : m_f(f, &fclose) {} + + ScopedFILE(ScopedFILE&& other) noexcept = default; + ~ScopedFILE() = default; + + ScopedFILE(const ScopedFILE&) = delete; + + void close() { m_f = nullptr; } + + operator bool() const { return static_cast(m_f); } + operator FILE*() const { return m_f.get(); } + +private: + std::unique_ptr m_f; +}; + +using std::filesystem::path; + +// path functions assume they are called by a test executable +// (calculate the requested path relative to the current executable path) + +path path_of_test_bin(const path& relative = path()); +path path_of_test_temp(const path& relative = path()); +path path_of_test_fixtures(const path& relative = path()); +path path_of_usvfs_lib(const path& relative = path()); + +std::string platform_dependant_executable(const char* name, const char* ext = "exe", + const char* platform = nullptr); + +// if full_path is a subfolder of base returns only the relative path, +// if full_path and base are the same folder "." is returned, +// otherwise full_path is returned unchanged +path path_as_relative(const path& base, const path& full_path); + +std::vector read_small_file(const path& file, bool binary = true); + +// true iff the the contents of the two files is exactly the same +bool compare_files(const path& file1, const path& file2, bool binary = true); + +// return true iff the given path is an empty (optionally true also if path doesn't +// exist) +bool is_empty_folder(const path& dpath, bool or_doesnt_exist = false); + +void delete_file(const path& file); + +// Recursively deletes the given path and all the files and directories under it +// Use with care!!! +void recursive_delete_files(const path& dpath); + +// Recursively copies all files and directories from srcPath to destPath +void recursive_copy_files(const path& src_path, const path& dest_path, bool overwrite); + +class ScopedLoadLibrary +{ +public: + ScopedLoadLibrary(const wchar_t* dll_path); + ~ScopedLoadLibrary(); + + // returns zero if load library failed + operator HMODULE() const { return m_mod; } + +private: + HMODULE m_mod; +}; +}; // namespace test diff --git a/libs/usvfs/test/thooklib_test/CMakeLists.txt b/libs/usvfs/test/thooklib_test/CMakeLists.txt new file mode 100644 index 0000000..cca9e10 --- /dev/null +++ b/libs/usvfs/test/thooklib_test/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) +find_package(Boost CONFIG REQUIRED COMPONENTS thread) + +# not sure why there is a test_hooks.cpp here? +add_executable(thooklib_test main.cpp) +usvfs_set_test_properties(thooklib_test) +target_link_libraries(thooklib_test + PRIVATE test_utils shared thooklib Boost::thread + GTest::gtest GTest::gtest_main) diff --git a/libs/usvfs/test/thooklib_test/main.cpp b/libs/usvfs/test/thooklib_test/main.cpp new file mode 100644 index 0000000..df48198 --- /dev/null +++ b/libs/usvfs/test/thooklib_test/main.cpp @@ -0,0 +1,235 @@ +#include +#include +#include +#include +#include +#include +// #include +#include +#include +#include +#include +#include + +namespace fs = boost::filesystem; + +#include + +using namespace std; +using namespace HookLib; + +class TempFile +{ +public: + TempFile(const wchar_t* relative) : wpath(winapi::wide::getModuleFileName(nullptr)) + { + size_t path_end = wpath.rfind(L'\\'); + if (path_end != std::wstring::npos) { + wpath.erase(path_end + 1); + wpath += L"..\\temp\\"; + wpath += relative; + } else + wpath = relative; + path = + usvfs::shared::string_cast(wpath, usvfs::shared::CodePage::UTF8); + } + + const char* c_str() const { return path.c_str(); } + const wchar_t* w_str() const { return wpath.c_str(); } + +private: + std::string path; + std::wstring wpath; +}; + +static const HANDLE MARKERHANDLE = reinterpret_cast(0x1CC0FFEE); +static const TempFile VALID_FILENAME{L"VALID_FILENAME"}; +static const TempFile INVALID_FILENAME{L"\\<>/"}; + +#include "test_hooks.cpp" + +static bool stubCalled = false; + +void __cdecl CreateFileStub(LPVOID) +{ + stubCalled = true; +} + +class HookingTest : public testing::Test +{ +public: + void SetUp() + { + /* typedef sinks::synchronous_sink text_sink; + boost::shared_ptr sink = boost::make_shared(); + + // Add a stream to write log to + sink->locked_backend()->add_stream(boost::make_shared("c:\\temp\\testing_out.log")); + + // Register the sink in the logging core + logging::core::get()->add_sink(sink); + + sink->set_filter(expr::attr("Severity") >= LogLevel::Debug);*/ + } + + void TearDown() {} + +private: +}; + +TEST(GetProcAddressTest, ReturnsValidResults) +{ + HMODULE mh = GetModuleHandleA("KernelBase.dll"); + EXPECT_NE(nullptr, mh); + EXPECT_EQ(GetProcAddress(mh, "CreateFileA"), MyGetProcAddress(mh, "CreateFileA")); +} + +TEST_F(HookingTest, CanHook) +{ + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallHook(k32Mod, "CreateFileW", THCreateFileW_1); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallHook(k32Mod, "CreateFileW", THCreateFileW_1); + } + EXPECT_NE(INVALID_HOOK, hook); + RemoveHook(hook); +} + +TEST_F(HookingTest, CanStub) +{ + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallStub(k32Mod, "CreateFileW", CreateFileStub); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallStub(k32Mod, "CreateFileW", CreateFileStub); + } + EXPECT_NE(INVALID_HOOK, hook); + RemoveHook(hook); +} + +TEST_F(HookingTest, RemoveHook) +{ + // test that we can remove a hook + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + } + + EXPECT_NE(INVALID_HOOK, hook); + RemoveHook(hook); + HANDLE test = CreateFileA(INVALID_FILENAME.c_str(), GENERIC_READ, 0, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + EXPECT_EQ(INVALID_HANDLE_VALUE, test); +} + +TEST_F(HookingTest, CreateFileStubTest) +{ + stubCalled = false; + // test if our stub works + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallStub(k32Mod, "CreateFileA", CreateFileStub); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallStub(k32Mod, "CreateFileA", CreateFileStub); + } + EXPECT_NE(INVALID_HOOK, hook); + HANDLE test = CreateFileA(INVALID_FILENAME.c_str(), GENERIC_READ, 0, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + RemoveHook(hook); + EXPECT_EQ(true, stubCalled); + EXPECT_EQ(INVALID_HANDLE_VALUE, test); +} + +TEST_F(HookingTest, CreateFileHook) +{ + // test if our hook works + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + } + HANDLE test = CreateFileA(INVALID_FILENAME.c_str(), GENERIC_READ, 0, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + RemoveHook(hook); + EXPECT_EQ(MARKERHANDLE, test); +} + +TEST_F(HookingTest, CreateFileWHook) +{ + // test if our hook works + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallHook(k32Mod, "CreateFileW", THCreateFileW_1); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallHook(k32Mod, "CreateFileW", THCreateFileW_1); + } + HANDLE test = CreateFileW(INVALID_FILENAME.w_str(), 0x42, 0x43, + (LPSECURITY_ATTRIBUTES)0x44, 0x45, 0x46, (HANDLE)0x47); + RemoveHook(hook); + EXPECT_EQ(MARKERHANDLE, test); +} + +TEST_F(HookingTest, CreateFileHookRecursion) +{ + // test that the trampoline works, so we can call the original function from + // within the hook + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + } + HANDLE test = CreateFileA(VALID_FILENAME.c_str(), GENERIC_READ, 0, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + RemoveHook(hook); + EXPECT_NE(MARKERHANDLE, test); +} + +TEST_F(HookingTest, Threading) +{ + // test that multiple threads can concurrently call a hooked function without + // incorrect results. + // TODO: this test doesn't reliably find thread-unsafeties + // NOTE: the hooklib currently does not claim that hook installation or removal + // is thread-safe, only the hooked functions shouldn't become less thread-safe by + // being hooked! + + static const int NUM_THREADS = 100; + static const int NUM_TRIES = 1000; + + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + } + std::thread threads[NUM_THREADS]; + for (int i = 0; i < NUM_THREADS; ++i) { + threads[i] = std::thread([i] { + for (int count = 0; count < NUM_TRIES; ++count) { + HANDLE test = CreateFileA(INVALID_FILENAME.c_str(), GENERIC_READ, 0, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + EXPECT_EQ(MARKERHANDLE, test); + } + }); + } + + for (int i = 0; i < NUM_THREADS; ++i) { + threads[i].join(); + } + + RemoveHook(hook); +} + +int main(int argc, char** argv) +{ + auto logger = spdlog::stdout_logger_mt("usvfs"); + logger->set_level(spdlog::level::warn); + TrampolinePool::initialize(); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/libs/usvfs/test/thooklib_test/test_hooks.cpp b/libs/usvfs/test/thooklib_test/test_hooks.cpp new file mode 100644 index 0000000..edde2c8 --- /dev/null +++ b/libs/usvfs/test/thooklib_test/test_hooks.cpp @@ -0,0 +1,38 @@ +#include +#include + +HANDLE WINAPI THCreateFileA_1(LPCSTR lpFileName, DWORD dwDesiredAccess, + DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, + DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, + HANDLE hTemplateFile) +{ + if (strcmp(lpFileName, INVALID_FILENAME.c_str()) == 0) { + return MARKERHANDLE; + } else { + HANDLE res = + ::CreateFileA(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, + dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + return res; + } +} + +HANDLE WINAPI THCreateFileW_1(LPCWSTR lpFileName, DWORD dwDesiredAccess, + DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, + DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, + HANDLE hTemplateFile) +{ + if (wcscmp(lpFileName, INVALID_FILENAME.w_str()) == 0) { + EXPECT_EQ(0x42, dwDesiredAccess); + EXPECT_EQ(0x43, dwShareMode); + EXPECT_EQ(0x44, (int)lpSecurityAttributes); + EXPECT_EQ(0x45, dwCreationDisposition); + EXPECT_EQ(0x46, dwFlagsAndAttributes); + EXPECT_EQ(0x47, (int)hTemplateFile); + return MARKERHANDLE; + } else { + return ::CreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, + dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + } +} diff --git a/libs/usvfs/test/tinjectlib_test/CMakeLists.txt b/libs/usvfs/test/tinjectlib_test/CMakeLists.txt new file mode 100644 index 0000000..9bd3f7b --- /dev/null +++ b/libs/usvfs/test/tinjectlib_test/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) + +# binary and binary injected +add_executable(testinject_bin testinject_bin/main.cpp) +usvfs_set_test_properties(testinject_bin FOLDER tinjectlib) + +add_library(testinject_dll SHARED testinject_dll/main.cpp testinject_dll/main.h) +usvfs_set_test_properties(testinject_dll FOLDER tinjectlib) + +# actual test executable +add_executable(tinjectlib_test main.cpp) +usvfs_set_test_properties(tinjectlib_test FOLDER tinjectlib) +target_link_libraries(tinjectlib_test PRIVATE tinjectlib shared GTest::gtest GTest::gtest_main) +add_dependencies(tinjectlib_test testinject_bin testinject_dll) diff --git a/libs/usvfs/test/tinjectlib_test/main.cpp b/libs/usvfs/test/tinjectlib_test/main.cpp new file mode 100644 index 0000000..ce48f83 --- /dev/null +++ b/libs/usvfs/test/tinjectlib_test/main.cpp @@ -0,0 +1,158 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace usvfs::shared; +using namespace InjectLib; + +#if BOOST_ARCH_X86_64 +static const wchar_t INJECT_BIN[] = L"testinject_bin_x64.exe"; +#else +static const wchar_t INJECT_BIN[] = L"testinject_bin_x86.exe"; +#endif + +#if BOOST_ARCH_X86_64 +static const wchar_t INJECT_LIB[] = L"testinject_dll_x64.dll"; +#else +static const wchar_t INJECT_LIB[] = L"testinject_dll_x86.dll"; +#endif + +static std::shared_ptr logger() +{ + std::shared_ptr result = spdlog::get("test"); + if (result.get() == nullptr) { + result = spdlog::stdout_logger_mt("test"); + } + return result; +} + +bool spawn(HANDLE& processHandle, HANDLE& threadHandle) +{ + STARTUPINFO si; + ::ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + + PROCESS_INFORMATION pi; + BOOL success = ::CreateProcess(INJECT_BIN, nullptr, nullptr, nullptr, FALSE, + CREATE_SUSPENDED, nullptr, nullptr, &si, &pi); + + if (!success) { + throw windows_error("failed to start process"); + } + + processHandle = pi.hProcess; + threadHandle = pi.hThread; + + return true; +} + +TEST(InjectingTest, InjectionNoInit) +{ + // Verify lib can inject without a init function + + HANDLE process, thread; + spawn(process, thread); + EXPECT_NO_THROW(InjectLib::InjectDLL(process, thread, INJECT_LIB)); + ResumeThread(thread); + + DWORD res = WaitForSingleObject(process, INFINITE); + DWORD exitCode = NO_ERROR; + res = GetExitCodeProcess(process, &exitCode); + EXPECT_EQ(NOERROR, exitCode); + + CloseHandle(process); + CloseHandle(thread); +} + +TEST(InjectingTest, InjectionSimpleInit) +{ + // Verify lib can inject with a init function with null parameters + + HANDLE process, thread; + spawn(process, thread); + EXPECT_NO_THROW(InjectLib::InjectDLL(process, thread, INJECT_LIB, "InitNoParam")); + ResumeThread(thread); + + DWORD res = WaitForSingleObject(process, INFINITE); + DWORD exitCode = NO_ERROR; + res = GetExitCodeProcess(process, &exitCode); + EXPECT_EQ(10001, exitCode); // used init function exits process with this exit code + + CloseHandle(process); + CloseHandle(thread); +} + +TEST(InjectingTest, InjectionComplexInit) +{ + // Verify lib can inject with a init function with null parameters + + static const WCHAR param[] = L"magic_parameter"; + HANDLE process, thread; + spawn(process, thread); + EXPECT_NO_THROW(InjectLib::InjectDLL(process, thread, INJECT_LIB, "InitComplexParam", + reinterpret_cast(param), + wcslen(param) * sizeof(WCHAR))); + + ResumeThread(thread); + + DWORD res = WaitForSingleObject(process, INFINITE); + DWORD exitCode = NO_ERROR; + res = GetExitCodeProcess(process, &exitCode); + EXPECT_EQ(10002, exitCode); // used init function exits process with this exit code + + CloseHandle(process); + CloseHandle(thread); +} + +TEST(InjectingTest, InjectionNoQuitInit) +{ + // Verify lib can inject with a init function with null parameters + + HANDLE process, thread; + spawn(process, thread); + EXPECT_NO_THROW(InjectLib::InjectDLL(process, thread, INJECT_LIB, "InitNoQuit")); + ResumeThread(thread); + + DWORD res = WaitForSingleObject(process, INFINITE); + DWORD exitCode = NO_ERROR; + res = GetExitCodeProcess(process, &exitCode); + EXPECT_EQ(0, exitCode); // expect regular exit from process + + CloseHandle(process); + CloseHandle(thread); +} + +TEST(InjectingTest, InjectionSkipInit) +{ + // verify the skip-on-missing mechanism for init function works + + HANDLE process, thread; + spawn(process, thread); + EXPECT_NO_THROW(InjectLib::InjectDLL(process, thread, INJECT_LIB, "__InitInvalid", + nullptr, 0, true)); + ResumeThread(thread); + + DWORD res = WaitForSingleObject(process, INFINITE); + DWORD exitCode = NO_ERROR; + res = GetExitCodeProcess(process, &exitCode); + EXPECT_EQ(NOERROR, exitCode); + + CloseHandle(process); + CloseHandle(thread); +} + +int main(int argc, char** argv) +{ + auto logger = spdlog::stdout_logger_mt("usvfs"); + logger->set_level(spdlog::level::warn); + + boost::filesystem::path filePath(winapi::wide::getModuleFileName(nullptr)); + SetCurrentDirectoryW(filePath.parent_path().wstring().c_str()); + + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/libs/usvfs/test/tinjectlib_test/testinject_bin/main.cpp b/libs/usvfs/test/tinjectlib_test/testinject_bin/main.cpp new file mode 100644 index 0000000..f8b643a --- /dev/null +++ b/libs/usvfs/test/tinjectlib_test/testinject_bin/main.cpp @@ -0,0 +1,4 @@ +int main() +{ + return 0; +} diff --git a/libs/usvfs/test/tinjectlib_test/testinject_dll/main.cpp b/libs/usvfs/test/tinjectlib_test/testinject_dll/main.cpp new file mode 100644 index 0000000..8339e97 --- /dev/null +++ b/libs/usvfs/test/tinjectlib_test/testinject_dll/main.cpp @@ -0,0 +1,36 @@ +#include "main.h" + +void __cdecl InitNoQuit(LPVOID, size_t) +{ + // nop +} + +void __cdecl InitNoParam(LPVOID, size_t) +{ + ExitProcess(10001); +} + +void __cdecl InitComplexParam(LPVOID userData, size_t) +{ + LPCWSTR string = (LPCWSTR)userData; + if (wcscmp(string, L"magic_parameter") == 0) { + ExitProcess(10002); + } else { + ExitProcess(20003); + } +} + +BOOL APIENTRY DllMain(HMODULE, DWORD reasonForCall, LPVOID) +{ + switch (reasonForCall) { + case DLL_PROCESS_ATTACH: { + } break; + case DLL_PROCESS_DETACH: { + } break; + case DLL_THREAD_ATTACH: { + } break; + case DLL_THREAD_DETACH: { + } break; + } + return TRUE; +} diff --git a/libs/usvfs/test/tinjectlib_test/testinject_dll/main.h b/libs/usvfs/test/tinjectlib_test/testinject_dll/main.h new file mode 100644 index 0000000..6faf800 --- /dev/null +++ b/libs/usvfs/test/tinjectlib_test/testinject_dll/main.h @@ -0,0 +1,11 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +extern "C" + __declspec(dllexport) void __cdecl InitNoQuit(LPVOID userData, size_t userDataSize); +extern "C" __declspec(dllexport) void __cdecl InitNoParam(LPVOID userData, + size_t userDataSize); +extern "C" __declspec(dllexport) void __cdecl InitComplexParam(LPVOID userData, + size_t userDataSize); diff --git a/libs/usvfs/test/tvfs_test/CMakeLists.txt b/libs/usvfs/test/tvfs_test/CMakeLists.txt new file mode 100644 index 0000000..f66a7b4 --- /dev/null +++ b/libs/usvfs/test/tvfs_test/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) + +add_executable(tvfs_test main.cpp) +usvfs_set_test_properties(tvfs_test) +target_link_libraries(tvfs_test PRIVATE test_utils usvfs_helper GTest::gtest GTest::gmock GTest::gtest_main) +usvfs_target_link_usvfs(tvfs_test) + +# tvfs_test uses a private USVFS header so we need to include it manually +get_target_property(USVFS_SOURCE_DIR usvfs_dll SOURCE_DIR) +target_include_directories(tvfs_test PRIVATE ${USVFS_SOURCE_DIR}) diff --git a/libs/usvfs/test/tvfs_test/main.cpp b/libs/usvfs/test/tvfs_test/main.cpp new file mode 100644 index 0000000..7c7998b --- /dev/null +++ b/libs/usvfs/test/tvfs_test/main.cpp @@ -0,0 +1,585 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see . +*/ + +// this file depends on so many stuff that the easiest way is to include +// pch.h from shared +#include "pch.h" + +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace spd = spdlog; + +namespace ush = usvfs::shared; + +// name of a file to be created in the virtual fs. Shouldn't exist on disc but the +// directory must exist +static LPCSTR VIRTUAL_FILEA = "C:/np.exe"; +static LPCWSTR VIRTUAL_FILEW = L"C:/np.exe"; + +// a real file on disc that has to exist +static LPCSTR REAL_FILEA = "C:/windows/notepad.exe"; +static LPCWSTR REAL_FILEW = L"C:/windows/notepad.exe"; + +static LPCSTR REAL_DIRA = "C:/windows/Logs"; +static LPCWSTR REAL_DIRW = L"C:/windows/Logs"; + +static std::shared_ptr logger() +{ + std::shared_ptr result = spdlog::get("test"); + if (result.get() == nullptr) { + result = spdlog::stdout_logger_mt("test"); + } + return result; +} + +auto defaultUsvfsParams(const char* instanceName = "usvfs_test") +{ + std::unique_ptr parameters{ + usvfsCreateParameters(), &usvfsFreeParameters}; + + usvfsSetInstanceName(parameters.get(), instanceName); + usvfsSetDebugMode(parameters.get(), true); + usvfsSetLogLevel(parameters.get(), LogLevel::Debug); + usvfsSetCrashDumpType(parameters.get(), CrashDumpsType::None); + usvfsSetCrashDumpPath(parameters.get(), ""); + + return std::move(parameters); +} + +class USVFSTest : public testing::Test +{ +public: + void SetUp() + { + SHMLogger::create("usvfs"); + // need to initialize logging in the context of the dll + usvfsInitLogging(); + } + + void TearDown() + { + std::array buffer; + while (SHMLogger::instance().tryGet(buffer.data(), buffer.size())) { + std::cout << buffer.data() << std::endl; + } + SHMLogger::free(); + } + +private: +}; + +class USVFSTestWithReroute : public testing::Test +{ +public: + void SetUp() + { + SHMLogger::create("usvfs"); + // need to initialize logging in the context of the dll + usvfsInitLogging(); + + auto params = defaultUsvfsParams(); + m_Context.reset(usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + usvfs::RedirectionTreeContainer& tree = m_Context->redirectionTable(); + tree.addFile( + ush::string_cast(VIRTUAL_FILEW, ush::CodePage::UTF8).c_str(), + usvfs::RedirectionDataLocal(REAL_FILEA)); + } + + void TearDown() + { + std::array buffer; + while (SHMLogger::instance().tryGet(buffer.data(), buffer.size())) { + std::cout << buffer.data() << std::endl; + } + m_Context.reset(); + SHMLogger::free(); + } + +private: + std::unique_ptr m_Context; +}; + +class USVFSTestAuto : public testing::Test +{ +public: + void SetUp() + { + auto params = defaultUsvfsParams(); + usvfsConnectVFS(params.get()); + SHMLogger::create("usvfs"); + } + + void TearDown() + { + usvfsDisconnectVFS(); + + std::array buffer; + while (SHMLogger::instance().tryGet(buffer.data(), buffer.size())) { + std::cout << buffer.data() << std::endl; + } + SHMLogger::free(); + } + +private: +}; + +TEST_F(USVFSTest, CanResizeRedirectiontree) +{ + using usvfs::shared::MissingThrow; + ASSERT_NO_THROW({ + usvfs::RedirectionTreeContainer container("treetest_shm", 1024); + for (char i = 'a'; i <= 'z'; ++i) { + for (char j = 'a'; j <= 'z'; ++j) { + std::string name = std::string(R"(C:\temp\)") + i + j; + container.addFile(name, usvfs::RedirectionDataLocal("gaga"), false); + } + } + + ASSERT_EQ("gaga", container->node("C:") + ->node("temp") + ->node("aa", MissingThrow) + ->data() + .linkTarget); + ASSERT_EQ("gaga", container->node("C:") + ->node("temp") + ->node("az", MissingThrow) + ->data() + .linkTarget); + }); +} + +/* +TEST_F(USVFSTest, CreateFileHookReportsCorrectErrorOnMissingFile) +{ + ASSERT_NO_THROW({ + USVFSParameters params; + USVFSInitParameters(¶ms, "usvfs_test", true, LogLevel::Debug, +CrashDumpsType::None, ""); std::unique_ptr +ctx(CreateHookContext(params, ::GetModuleHandle(nullptr))); HANDLE res = +usvfs::hook_CreateFileW(VIRTUAL_FILEW , GENERIC_READ , FILE_SHARE_READ | +FILE_SHARE_WRITE , nullptr , OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , nullptr); + + ASSERT_EQ(INVALID_HANDLE_VALUE, res); + ASSERT_EQ(ERROR_FILE_NOT_FOUND, ::GetLastError()); + }); +} +*/ + +/* +TEST_F(USVFSTestWithReroute, CreateFileHookRedirectsFile) +{ + ASSERT_NE(INVALID_HANDLE_VALUE + , usvfs::hook_CreateFileW(VIRTUAL_FILEW + , GENERIC_READ + , FILE_SHARE_READ | FILE_SHARE_WRITE + , nullptr + , OPEN_EXISTING + , FILE_ATTRIBUTE_NORMAL + , nullptr)); +} +*/ + +TEST_F(USVFSTest, GetFileAttributesHookReportsCorrectErrorOnMissingFile) +{ + ASSERT_NO_THROW({ + try { + auto params = defaultUsvfsParams(); + std::unique_ptr ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + DWORD res = usvfs::hook_GetFileAttributesW(VIRTUAL_FILEW); + + ASSERT_EQ(INVALID_FILE_ATTRIBUTES, res); + ASSERT_EQ(ERROR_FILE_NOT_FOUND, ::GetLastError()); + } catch (const std::exception& e) { + logger()->error("Exception: {}", e.what()); + throw; + } + }); +} + +TEST_F(USVFSTest, GetFileAttributesHookRedirectsFile) +{ + auto params = defaultUsvfsParams(); + std::unique_ptr ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); + + tree.addFile( + ush::string_cast(VIRTUAL_FILEW, ush::CodePage::UTF8).c_str(), + usvfs::RedirectionDataLocal(REAL_FILEA)); + + ASSERT_EQ(::GetFileAttributesW(REAL_FILEW), + usvfs::hook_GetFileAttributesW(VIRTUAL_FILEW)); +} +/* +TEST_F(USVFSTest, GetFullPathNameOnRegularCurrentDirectory) +{ + USVFSParameters params; + USVFSInitParameters(¶ms, "usvfs_test", true, LogLevel::Debug, +CrashDumpsType::None, ""); std::unique_ptr +ctx(CreateHookContext(params, ::GetModuleHandle(nullptr))); + + std::wstring expected = winapi::wide::getCurrentDirectory() + L"\\filename.txt"; + + DWORD bufferLength = 32767; + std::unique_ptr buffer(new wchar_t[bufferLength]); + LPWSTR filePart = nullptr; + + DWORD res = usvfs::hook_GetFullPathNameW(L"filename.txt", bufferLength, buffer.get(), +&filePart); + + ASSERT_NE(0UL, res); + ASSERT_EQ(expected, std::wstring(buffer.get())); +}*/ + +// small wrapper to call usvfs::hook_NtOpenFile with a path +// +// at some point in time, changes were made to USVFS such that calling a hooked +// function from a handle obtained from a non-hooked function would not work anymore, +// meaning that function such as CreateFileW that have no hook equivalent cannot +// be used to test hook functions +// +// this function is useful to simulate a CreateFileW by internally using the hook +// version of NtOpenFile +// +HANDLE hooked_NtOpenFile(LPCWSTR path, ACCESS_MASK accessMask, ULONG shareAccess, + ULONG openOptions) +{ + constexpr size_t BUFFER_SIZE = 2048; + IO_STATUS_BLOCK statusBlock; + OBJECT_ATTRIBUTES attributes; + attributes.SecurityDescriptor = 0; + attributes.SecurityQualityOfService = 0; + attributes.RootDirectory = 0; + attributes.Attributes = 0; + attributes.Length = sizeof(OBJECT_ATTRIBUTES); + + WCHAR stringBuffer[BUFFER_SIZE]; + UNICODE_STRING string; + string.Buffer = stringBuffer; + lstrcpyW(stringBuffer, L"\\??\\"); + lstrcatW(stringBuffer, path); + string.Length = static_cast(lstrlenW(stringBuffer) * 2); + string.MaximumLength = BUFFER_SIZE; + attributes.ObjectName = &string; + + HANDLE ret = INVALID_HANDLE_VALUE; + if (usvfs::hook_NtOpenFile(&ret, accessMask, &attributes, &statusBlock, shareAccess, + openOptions) != STATUS_SUCCESS) { + return INVALID_HANDLE_VALUE; + } + + return ret; +} + +TEST_F(USVFSTest, NtQueryDirectoryFileRegularFile) +{ + auto params = defaultUsvfsParams(); + std::unique_ptr ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + + HANDLE hdl = + hooked_NtOpenFile(L"C:\\", FILE_GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); + ASSERT_NE(INVALID_HANDLE_VALUE, hdl); + + IO_STATUS_BLOCK status; + char buffer[1024]; + + usvfs::hook_NtQueryDirectoryFile(hdl, nullptr, nullptr, nullptr, &status, buffer, + 1024, FileDirectoryInformation, TRUE, nullptr, TRUE); + + ASSERT_EQ(STATUS_SUCCESS, status.Status); + + usvfs::hook_NtClose(hdl); +} + +TEST_F(USVFSTest, NtQueryDirectoryFileFindsVirtualFile) +{ + auto params = defaultUsvfsParams(); + std::unique_ptr ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); + + tree.addFile(L"C:\\np.exe", usvfs::RedirectionDataLocal(REAL_FILEA)); + + HANDLE hdl = + hooked_NtOpenFile(L"C:\\", FILE_GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); + ASSERT_NE(INVALID_HANDLE_VALUE, hdl); + + IO_STATUS_BLOCK status; + char buffer[1024]; + + usvfs::UnicodeString fileName(L"np.exe"); + + usvfs::hook_NtQueryDirectoryFile(hdl, nullptr, nullptr, nullptr, &status, buffer, + 1024, FileDirectoryInformation, TRUE, + static_cast(fileName), TRUE); + + FILE_DIRECTORY_INFORMATION* info = + reinterpret_cast(buffer); + ASSERT_EQ(STATUS_SUCCESS, status.Status); + ASSERT_EQ(0, wcscmp(info->FileName, L"np.exe")); + + usvfs::hook_NtClose(hdl); +} + +TEST_F(USVFSTest, NtQueryDirectoryFileExVirtualFile) +{ + auto params = defaultUsvfsParams(); + std::unique_ptr ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); + + tree.addFile(L"C:\\0123456789.txt", usvfs::RedirectionDataLocal(REAL_FILEA)); + tree.addFile(L"C:\\123456", usvfs::RedirectionDataLocal(REAL_FILEA)); + tree.addFile(L"C:\\abcdef", usvfs::RedirectionDataLocal(REAL_FILEA)); + tree.addFile(L"C:\\abcdefghijklmnopqrstuvwxyz.txt", + usvfs::RedirectionDataLocal(REAL_FILEA)); + + HANDLE hdl = + hooked_NtOpenFile(L"C:\\", FILE_GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); + ASSERT_NE(INVALID_HANDLE_VALUE, hdl); + + IO_STATUS_BLOCK status; + + constexpr size_t BUFFER_SIZE = 2048; + char buffer[BUFFER_SIZE]; + + std::vector foundFiles; + while (usvfs::hook_NtQueryDirectoryFileEx( + hdl, nullptr, nullptr, nullptr, &status, buffer, BUFFER_SIZE, + FileFullDirectoryInformation, 0, nullptr) == STATUS_SUCCESS) { + std::size_t offset = 0; + while (offset < BUFFER_SIZE) { + const auto* info = reinterpret_cast(buffer + offset); + foundFiles.emplace_back(info->FileName, info->FileNameLength / sizeof(wchar_t)); + + if (info->NextEntryOffset == 0) { + break; // no more entries + } + + offset += info->NextEntryOffset; + } + } + + ASSERT_THAT(foundFiles, + ::testing::IsSupersetOf({L"0123456789.txt", L"123456", L"abcdef", + L"abcdefghijklmnopqrstuvwxyz.txt"})); + + usvfs::hook_NtClose(hdl); +} + +TEST_F(USVFSTest, NtQueryObjectVirtualFile) +{ + std::wstring c_drive_device; + { + // find the device path for C: + HANDLE hdl = ::CreateFileW( + L"C:\\", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + ASSERT_NE(INVALID_HANDLE_VALUE, hdl); + + char buffer[2048]; + ASSERT_EQ(STATUS_SUCCESS, ::NtQueryObject(hdl, ObjectNameInformation, buffer, + sizeof(buffer), nullptr)); + + OBJECT_NAME_INFORMATION* information = + reinterpret_cast(buffer); + + c_drive_device = + std::wstring(information->Name.Buffer, information->Name.Length / 2); + + ::CloseHandle(hdl); + } + + auto params = defaultUsvfsParams(); + std::unique_ptr ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); + + tree.addFile(L"C:\\np.exe", usvfs::RedirectionDataLocal(REAL_FILEA)); + + HANDLE hdl = hooked_NtOpenFile(L"C:\\np.exe", FILE_GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_NON_DIRECTORY_FILE | FILE_OPEN_FOR_BACKUP_INTENT); + ASSERT_NE(INVALID_HANDLE_VALUE, hdl) << "last error=" << ::GetLastError(); + + { + char buffer[1024]; + IO_STATUS_BLOCK status; + const auto res = usvfs::hook_NtQueryInformationFile( + hdl, &status, buffer, sizeof(buffer), FileNameInformation); + ASSERT_EQ(STATUS_SUCCESS, res); + ASSERT_EQ(STATUS_SUCCESS, status.Status); + + FILE_NAME_INFORMATION* fileNameInfo = + reinterpret_cast(buffer); + ASSERT_EQ(L"\\np.exe", + std::wstring(fileNameInfo->FileName, fileNameInfo->FileNameLength / 2)); + } + + { + char buffer[1024]; + IO_STATUS_BLOCK status; + const auto res = usvfs::hook_NtQueryInformationFile( + hdl, &status, buffer, sizeof(buffer), FileNormalizedNameInformation); + ASSERT_EQ(STATUS_SUCCESS, res); + ASSERT_EQ(STATUS_SUCCESS, status.Status); + ASSERT_EQ(sizeof(ULONG) + 7 * 2, status.Information); + + FILE_NAME_INFORMATION* fileNameInfo = + reinterpret_cast(buffer); + ASSERT_EQ(L"\\np.exe", + std::wstring(fileNameInfo->FileName, fileNameInfo->FileNameLength / 2)); + } + + // buffer of size should be too small for the original path (\Windows\notepad.exe) + // but not for \np.exe + { + // the required size should be sizeof(ULONG) + 7 * 2 but apparently that is + // not enough for the CI so using 16 * 2 which should be large enough for + // the hooked version, but still too short for the non-hooked one + char buffer[sizeof(ULONG) + 7 * 2]; + IO_STATUS_BLOCK status; + NTSTATUS res; + + res = ::NtQueryInformationFile(hdl, &status, buffer, sizeof(buffer), + FileNameInformation); + ASSERT_EQ(STATUS_BUFFER_OVERFLOW, res); + ASSERT_EQ(STATUS_BUFFER_OVERFLOW, status.Status); + + res = usvfs::hook_NtQueryInformationFile(hdl, &status, buffer, sizeof(buffer), + FileNameInformation); + ASSERT_EQ(STATUS_SUCCESS, res); + ASSERT_EQ(STATUS_SUCCESS, status.Status); + ASSERT_EQ(sizeof(ULONG) + 7 * 2, status.Information); + + FILE_NAME_INFORMATION* fileNameInfo = + reinterpret_cast(buffer); + ASSERT_EQ(L"\\np.exe", + std::wstring(fileNameInfo->FileName, fileNameInfo->FileNameLength / 2)); + } + + { + char buffer[2048]; + const auto res = usvfs::hook_NtQueryObject(hdl, ObjectNameInformation, buffer, + sizeof(buffer), nullptr); + ASSERT_EQ(STATUS_SUCCESS, res); + + OBJECT_NAME_INFORMATION* information = + reinterpret_cast(buffer); + ASSERT_EQ(c_drive_device + L"np.exe", + std::wstring(information->Name.Buffer, + information->Name.Length / sizeof(wchar_t))); + } + + { + // expected length is sizeof struct + size of path (in bytes), including the + // null-character + const auto expectedLength = + sizeof(OBJECT_NAME_INFORMATION) + c_drive_device.size() * 2 + 12 + 2; + ULONG requiredLength; + NTSTATUS res; + char buffer[2048]; + + res = + usvfs::hook_NtQueryObject(hdl, ObjectNameInformation, buffer, + sizeof(OBJECT_NAME_INFORMATION) - 1, &requiredLength); + ASSERT_EQ(STATUS_INFO_LENGTH_MISMATCH, res); + ASSERT_EQ(expectedLength, requiredLength); + + res = usvfs::hook_NtQueryObject(hdl, ObjectNameInformation, buffer, + sizeof(OBJECT_NAME_INFORMATION), &requiredLength); + ASSERT_EQ(STATUS_BUFFER_OVERFLOW, res); + ASSERT_EQ(expectedLength, requiredLength); + } + + usvfs::hook_NtClose(hdl); +} + +TEST_F(USVFSTestAuto, CannotCreateLinkToFileInNonexistantDirectory) +{ + ASSERT_EQ(FALSE, usvfsVirtualLinkFile( + REAL_FILEW, L"c:/this_directory_shouldnt_exist/np.exe", FALSE)); +} + +TEST_F(USVFSTestAuto, CanCreateMultipleLinks) +{ + static LPCWSTR outFile = LR"(C:\np.exe)"; + static LPCWSTR outDir = LR"(C:\logs)"; + static LPCWSTR outDirCanonizeTest = LR"(C:\.\not/../logs\.\a\.\b\.\c\..\.\..\.\..\)"; + ASSERT_EQ(TRUE, usvfsVirtualLinkFile(REAL_FILEW, outFile, 0)); + ASSERT_EQ(TRUE, usvfsVirtualLinkDirectoryStatic(REAL_DIRW, outDir, 0)); + + // both file and dir exist and have the correct type + ASSERT_NE(INVALID_FILE_ATTRIBUTES, usvfs::hook_GetFileAttributesW(outFile)); + ASSERT_NE(INVALID_FILE_ATTRIBUTES, usvfs::hook_GetFileAttributesW(outDir)); + ASSERT_EQ(0UL, usvfs::hook_GetFileAttributesW(outFile) & FILE_ATTRIBUTE_DIRECTORY); + ASSERT_NE(0UL, usvfs::hook_GetFileAttributesW(outDir) & FILE_ATTRIBUTE_DIRECTORY); + ASSERT_NE(0UL, usvfs::hook_GetFileAttributesW(outDirCanonizeTest) & + FILE_ATTRIBUTE_DIRECTORY); +} + +int main(int argc, char** argv) +{ + using namespace test; + + auto dllPath = path_of_usvfs_lib(platform_dependant_executable("usvfs", "dll")); + ScopedLoadLibrary loadDll(dllPath.c_str()); + if (!loadDll) { + std::wcerr << L"failed to load usvfs dll: " << dllPath.c_str() << L", " + << GetLastError() << std::endl; + return 1; + } + + // note: this makes the logger available only to functions statically linked to the + // test binary, not those called in the dll + auto logger = spdlog::stdout_logger_mt("usvfs"); + logger->set_level(spdlog::level::warn); + testing::InitGoogleTest(&argc, argv); + int res = RUN_ALL_TESTS(); + + return res; +} diff --git a/libs/usvfs/test/usvfs_global_test_runner/CMakeLists.txt b/libs/usvfs/test/usvfs_global_test_runner/CMakeLists.txt new file mode 100644 index 0000000..88ff0c2 --- /dev/null +++ b/libs/usvfs/test/usvfs_global_test_runner/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) +find_package(Boost CONFIG REQUIRED COMPONENTS filesystem) + +# other executables +add_executable(usvfs_global_test + usvfs_global_test/usvfs_global_test.cpp +) +usvfs_set_test_properties(usvfs_global_test FOLDER usvfs_global_test) +target_link_libraries(usvfs_global_test PRIVATE gtest_utils GTest::gtest_main GTest::gmock Boost::filesystem) + +# actual test executable +add_executable(usvfs_global_test_runner + usvfs_global_test_fixture.cpp + usvfs_global_test_fixture.h + usvfs_global_test_runner.cpp +) +usvfs_set_test_properties(usvfs_global_test_runner FOLDER usvfs_global_test) +usvfs_target_link_usvfs(usvfs_global_test_runner) +target_link_libraries(usvfs_global_test_runner + PRIVATE test_utils gtest_utils GTest::gtest_main) +add_dependencies(usvfs_global_test_runner usvfs_global_test) diff --git a/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test/usvfs_global_test.cpp b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test/usvfs_global_test.cpp new file mode 100644 index 0000000..79c6c4c --- /dev/null +++ b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test/usvfs_global_test.cpp @@ -0,0 +1,216 @@ +#include + +#define WIN32_LEAN_AND_MEAN +#include + +#include +#include + +#include + +#include +#include + +// anonymous class that allow tests to access parameters (currently only where the +// virtualized data folder is) +// +class +{ + bool initialize(int argc, char* argv[]) + { + // extract the data path + if (argc != 2) { + std::cerr << "missing data path, aborting\n"; + return false; + } + m_data = std::filesystem::path{argv[1]}; + + if (!exists(m_data)) { + std::cerr << "data path '" << m_data.string() << "'does not exist\n"; + return false; + } + + return true; + } + + friend int main(int argc, char* argv[]); + std::filesystem::path m_data; + +public: + const auto& data() const { return m_data; } +} parameters; + +// simple guard for handle +class HandleGuard +{ + HANDLE m_handle = INVALID_HANDLE_VALUE; + +public: + HandleGuard() = default; + HandleGuard(HANDLE handle) : m_handle{handle} {} + + ~HandleGuard() { close(); } + + operator HANDLE() { return m_handle; } + + void close() + { + if (m_handle != INVALID_HANDLE_VALUE) { + ::CloseHandle(m_handle); + m_handle = INVALID_HANDLE_VALUE; + } + } +}; + +// simple function to write content to a specified path +void write_content(const std::filesystem::path& path, const std::string_view content) +{ + std::ofstream ofs{path}; + ofs << content; +} + +TEST(BasicTest, SimpleTest) +{ + const auto data = parameters.data(); + + ASSERT_TRUE(exists(data)); + ASSERT_TRUE(exists(data / "docs")); + ASSERT_TRUE(exists(data / "empty")); + ASSERT_TRUE(exists(data / "docs" / "doc.txt")); + ASSERT_TRUE(exists(data / "readme.txt")); + + // should remove mods/mod1/info.txt + ASSERT_TRUE(exists(data / "info.txt")); + remove(data / "info.txt"); + ASSERT_FALSE(exists(data / "info.txt")); + + { + // retrieve the path of data using GetFinalPathNameByHandleW() to + // compare later on + std::filesystem::path dataPathFromHandle; + { + HandleGuard hdl = CreateFileW(data.c_str(), GENERIC_READ, 0, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + ASSERT_NE(INVALID_HANDLE_VALUE, (HANDLE)hdl); + + WCHAR filepath[1024]; + const auto length = GetFinalPathNameByHandleW( + hdl, filepath, sizeof(filepath) / sizeof(WCHAR), FILE_NAME_NORMALIZED); + ASSERT_NE(0, length); + + dataPathFromHandle = std::filesystem::path(std::wstring(filepath, length)); + } + + const auto doc_txt = data / "docs" / "doc.txt"; + HandleGuard hdl = CreateFileW(doc_txt.c_str(), GENERIC_READ, 0, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + ASSERT_NE(INVALID_HANDLE_VALUE, (HANDLE)hdl); + + WCHAR filepath[1024]; + const auto length = GetFinalPathNameByHandleW( + hdl, filepath, sizeof(filepath) / sizeof(WCHAR), FILE_NAME_NORMALIZED); + ASSERT_NE(0, length); + + ASSERT_EQ(dataPathFromHandle / "docs" / "doc.txt", + std::filesystem::path(std::wstring(filepath, length))); + } + + { + // retrieve the path of data using GetFileInformationByHandleEx() to + // compare later on + std::filesystem::path dataPathFromHandle; + { + HandleGuard hdl = CreateFileW(data.c_str(), GENERIC_READ, 0, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + ASSERT_NE(INVALID_HANDLE_VALUE, (HANDLE)hdl); + + char buffer[4096]; + ASSERT_TRUE( + GetFileInformationByHandleEx(hdl, FileNameInfo, buffer, sizeof(buffer))); + auto* info = reinterpret_cast(buffer); + + dataPathFromHandle = std::filesystem::path( + std::wstring_view(info->FileName, info->FileNameLength / 2)); + } + + const auto info_txt = data / "readme.txt"; + HandleGuard hdl = CreateFileW(info_txt.c_str(), GENERIC_READ, 0, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + ASSERT_NE(INVALID_HANDLE_VALUE, (HANDLE)hdl); + + char buffer[4096]; + ASSERT_TRUE( + GetFileInformationByHandleEx(hdl, FileNameInfo, buffer, sizeof(buffer))); + + auto* info = reinterpret_cast(buffer); + + ASSERT_EQ(dataPathFromHandle / "readme.txt", + std::filesystem::path( + std::wstring_view(info->FileName, info->FileNameLength / 2))); + } +} + +TEST(BoostFilesystemTest, BoostFilesystemTest) +{ + using namespace boost::filesystem; + + const path data{parameters.data().native()}; + + std::vector contents; + for (recursive_directory_iterator it{data}; it != recursive_directory_iterator{}; + ++it) { + contents.push_back(relative(it->path(), data).string()); + } + ASSERT_THAT(contents, ::testing::UnorderedElementsAre( + ".gitkeep", "docs", "docs\\doc.txt", "docs\\subdocs", + "docs\\subdocs\\.gitkeep")); +} + +// see https://github.com/ModOrganizer2/modorganizer/issues/2039 for context +// +TEST(RedFileSystemTest, RedFileSystemTest) +{ + const auto data = parameters.data(); + + const auto storages_path = data / "r6" / "storages"; + const auto hudpainter_path = storages_path / "HUDPainter"; + + ASSERT_TRUE(exists(hudpainter_path / "DEFAULT.json")); + + // TEST.json does not exist, so will be created in overwrite - this mainly check that + // weakly_canonical returns the path under data/ and not the actual path under + // overwrite/ + // + // this relies on the hook for NtQueryInformationFile + // + ASSERT_FALSE(exists(hudpainter_path / "TEST.json")); + ASSERT_EQ(hudpainter_path / "TEST.json", + weakly_canonical(hudpainter_path / "TEST.json")); + write_content(hudpainter_path / "TEST.json", "{}"); +} + +TEST(SkipFilesTest, SkipFilesTest) +{ + const auto data = parameters.data(); + + // file in mod1 should have been skipped + ASSERT_FALSE(exists(data / "readme.skip")); + + // docs/doc.skip should come from data, not mods1/docs/doc.skip + ASSERT_CONTENT_EQ("doc.skip in data/docs", data / "docs" / "doc.skip"); + + // write to readme.skip should create a file in overwrite + write_content(data / "readme.skip", "readme.skip in overwrite"); +} + +int main(int argc, char* argv[]) +{ + testing::InitGoogleTest(&argc, argv); + + // initialize parameters from remaining arguments + if (!parameters.initialize(argc, argv)) { + return -1; + } + + return RUN_ALL_TESTS(); +} diff --git a/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp new file mode 100644 index 0000000..4f6e672 --- /dev/null +++ b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp @@ -0,0 +1,205 @@ +#include "usvfs_global_test_fixture.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +// find the path to the executable that contains gtest entries +// +std::filesystem::path path_to_usvfs_global_test() +{ + return test::path_of_test_bin( + test::platform_dependant_executable("usvfs_global_test", "exe")); +} + +// path to the fixture for the given test group +// +std::filesystem::path path_to_usvfs_global_test_figures(std::wstring_view group) +{ + return test::path_of_test_fixtures() / "usvfs_global_test" / group; +} + +// spawn the an hook version of the given +// +DWORD spawn_usvfs_hooked_process( + const std::filesystem::path& app, const std::vector& arguments = {}, + const std::optional& working_directory = {}) +{ + using namespace usvfs::shared; + + std::wstring command = app; + std::filesystem::path cwd = working_directory.value_or(app.parent_path()); + std::vector env; + + if (!arguments.empty()) { + command += L" " + boost::algorithm::join(arguments, L" "); + } + + STARTUPINFO si{0}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi{0}; + +#pragma warning(push) +#pragma warning(disable : 6387) + + if (!usvfsCreateProcessHooked(nullptr, command.data(), nullptr, nullptr, FALSE, 0, + nullptr, cwd.c_str(), &si, &pi)) { + test::throw_testWinFuncFailed( + "CreateProcessHooked", + string_cast(command, CodePage::UTF8).c_str()); + } + + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD exit = 99; + if (!GetExitCodeProcess(pi.hProcess, &exit)) { + test::WinFuncFailedGenerator failed; + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + throw failed("GetExitCodeProcess"); + } + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + +#pragma warning(pop) + + return exit; +} + +bool UsvfsGlobalTest::m_force_usvfs_logs = false; + +void UsvfsGlobalTest::ForceUsvfsLogs() +{ + m_force_usvfs_logs = true; +} + +class UsvfsGlobalTest::UsvfsGuard +{ +public: + UsvfsGuard(std::string_view instance_name = "usvfs_test", bool logging = false) + : m_parameters(usvfsCreateParameters(), &usvfsFreeParameters) + { + usvfsSetInstanceName(m_parameters.get(), instance_name.data()); + usvfsSetDebugMode(m_parameters.get(), false); + usvfsSetLogLevel(m_parameters.get(), LogLevel::Debug); + usvfsSetCrashDumpType(m_parameters.get(), CrashDumpsType::None); + usvfsSetCrashDumpPath(m_parameters.get(), ""); + + usvfsInitLogging(logging); + usvfsCreateVFS(m_parameters.get()); + } + + ~UsvfsGuard() { usvfsDisconnectVFS(); } + +private: + std::unique_ptr m_parameters; +}; + +UsvfsGlobalTest::UsvfsGlobalTest() + : m_usvfs{std::make_unique()}, + m_temporary_folder{test::path_of_test_temp()} +{ + std::string name{testing::UnitTest::GetInstance()->current_test_info()->name()}; + m_group = {name.begin(), name.end()}; +} + +UsvfsGlobalTest::~UsvfsGlobalTest() +{ + CleanUp(); +} + +std::filesystem::path UsvfsGlobalTest::ActualFolder() const +{ + return m_temporary_folder; +} + +std::filesystem::path UsvfsGlobalTest::ExpectedFolder() const +{ + return path_to_usvfs_global_test_figures(m_group) / "expected"; +} + +void UsvfsGlobalTest::CleanUp() const +{ + if (exists(m_temporary_folder)) { + remove_all(m_temporary_folder); + } +} + +void UsvfsGlobalTest::PrepareFileSystem() const +{ + // cleanup in case a previous tests failed to delete its stuff + CleanUp(); + + // copy fixtures + const auto fixtures = path_to_usvfs_global_test_figures(m_group) / "source"; + if (exists(fixtures)) { + copy(fixtures, m_temporary_folder, std::filesystem::copy_options::recursive); + } +} + +void UsvfsGlobalTest::SetUpOverwrite(bool force) const +{ + if (force && !exists(m_overwrite_folder)) { + create_directory(m_overwrite_folder); + } + + if (exists(m_overwrite_folder)) { + usvfsVirtualLinkDirectoryStatic(m_overwrite_folder.c_str(), m_data_folder.c_str(), + LINKFLAG_CREATETARGET | LINKFLAG_RECURSIVE); + } +} + +void UsvfsGlobalTest::PrepareMapping() const +{ + // should not be needed, but just to be safe + usvfsClearVirtualMappings(); + + if (!exists(m_data_folder)) { + throw std::runtime_error{ + std::format("data path missing at {}", m_data_folder.string())}; + } + + if (exists(m_mods_folder)) { + for (const auto& mod : std::filesystem::directory_iterator(m_mods_folder)) { + if (!is_directory(mod)) { + continue; + } + usvfsVirtualLinkDirectoryStatic(mod.path().c_str(), m_data_folder.c_str(), + LINKFLAG_RECURSIVE); + } + } + + // by default, only create overwrite if present + SetUpOverwrite(false); +} + +int UsvfsGlobalTest::Run() const +{ + PrepareMapping(); + + const auto res = spawn_usvfs_hooked_process( + path_to_usvfs_global_test(), {std::format(L"--gtest_filter={}.*", m_group), + L"--gtest_brief=1", m_data_folder.native()}); + + // TODO: try to do this with gtest itself? + if (m_force_usvfs_logs || res != 0) { + const auto log_path = test::path_of_test_bin(m_group + L".log"); + std::ofstream os{log_path}; + std::string buffer(1024, '\0'); + std::cout << "process returned " << std::hex << res << ", usvfs logs dumped to " + << log_path.string() << '\n'; + while (usvfsGetLogMessages(buffer.data(), buffer.size(), false)) { + os << " " << buffer.c_str() << "\n"; + } + } + + return res; +} diff --git a/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.h b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.h new file mode 100644 index 0000000..aeafc25 --- /dev/null +++ b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include + +#include + +class UsvfsGlobalTest : public testing::Test +{ +public: + // enable log mode - this will generate USVFS log file for all tests regardless of + // success or failure (default is to only generate for failure) + // + static void ForceUsvfsLogs(); + +public: + UsvfsGlobalTest(); + + void SetUp() override { PrepareFileSystem(); } + + void TearDown() override { CleanUp(); } + + ~UsvfsGlobalTest(); + + // setup the overwrite folder + // + // if force is true, force creation of the overwrite folder even if not present, this + // is useful when the overwrite is initially empty and thus cannot be committed to git + // but need to contain files after the run + // + void SetUpOverwrite(bool force = true) const; + + // run the test, return the exit code of the google test process + // + int Run() const; + + // return the path to the folder containing the expected results + // + std::filesystem::path ActualFolder() const; + + // return the path to the folder containing the expected results + // + std::filesystem::path ExpectedFolder() const; + +private: + class UsvfsGuard; + + // always generate usvfs logs + static bool m_force_usvfs_logs; + + // prepare the filesystem by copying files and folders from the relevant fixtures + // folder to the temporary folder + // + // after this operations, the temporary folder will contain + // - a data folder + // - [optional] a mods folder containing a set of folders that should be mounted + // - [optional] an overwrite folder that should be mounted as overwrite + // + void PrepareFileSystem() const; + + // prepare mapping using the given set of paths + // + void PrepareMapping() const; + + // cleanup the temporary path + // + void CleanUp() const; + + // usvfs_guard + std::unique_ptr m_usvfs; + + // name of GTest group (first argument of the TEST macro) to run + std::wstring m_group; + + // path to the folder containing temporary files + std::filesystem::path m_temporary_folder; + + // path to the subfolder inside the temporary folder + std::filesystem::path m_data_folder = m_temporary_folder / "data"; + std::filesystem::path m_mods_folder = m_temporary_folder / "mods"; + std::filesystem::path m_overwrite_folder = m_temporary_folder / "overwrite"; +}; diff --git a/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp new file mode 100644 index 0000000..2fa6cc8 --- /dev/null +++ b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp @@ -0,0 +1,58 @@ +#include + +#include +#include +#include + +#include "usvfs_global_test_fixture.h" + +TEST_F(UsvfsGlobalTest, BasicTest) +{ + ASSERT_EQ(0, Run()); + ASSERT_DIRECTORY_EQ(ExpectedFolder(), ActualFolder()); +} + +TEST_F(UsvfsGlobalTest, BoostFilesystemTest) +{ + ASSERT_EQ(0, Run()); + ASSERT_DIRECTORY_EQ(ExpectedFolder(), ActualFolder()); +} + +TEST_F(UsvfsGlobalTest, RedFileSystemTest) +{ + SetUpOverwrite(true); + ASSERT_EQ(0, Run()); + ASSERT_DIRECTORY_EQ(ExpectedFolder(), ActualFolder()); +} + +TEST_F(UsvfsGlobalTest, SkipFilesTest) +{ + SetUpOverwrite(true); + + usvfsAddSkipFileSuffix(L".skip"); + + ASSERT_EQ(0, Run()); + ASSERT_DIRECTORY_EQ(ExpectedFolder(), ActualFolder()); +} + +int main(int argc, char* argv[]) +{ + // load the USVFS DLL + // + const auto usvfs_dll = + test::path_of_usvfs_lib(test::platform_dependant_executable("usvfs", "dll")); + test::ScopedLoadLibrary loadDll(usvfs_dll.c_str()); + if (!loadDll) { + std::wcerr << L"ERROR: failed to load usvfs dll: " << usvfs_dll.c_str() << L", " + << GetLastError() << L"\n"; + return 3; + } + + testing::InitGoogleTest(&argc, argv); + + UsvfsGlobalTest::ForceUsvfsLogs(); + + usvfsInitLogging(false); + + return RUN_ALL_TESTS(); +} diff --git a/libs/usvfs/test/usvfs_test_runner/CMakeLists.txt b/libs/usvfs/test/usvfs_test_runner/CMakeLists.txt new file mode 100644 index 0000000..a0a7ca9 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) + +# other executables +add_executable(test_file_operations + test_file_operations/test_file_operations.cpp + test_file_operations/test_filesystem.cpp + test_file_operations/test_filesystem.h + test_file_operations/test_ntapi.cpp + test_file_operations/test_ntapi.h + test_file_operations/test_ntdll_declarations.h + test_file_operations/test_w32api.cpp + test_file_operations/test_w32api.h +) +usvfs_set_test_properties(test_file_operations FOLDER usvfs_test_runner) +target_link_libraries(test_file_operations PRIVATE test_utils ntdll) + +add_executable(usvfs_test + usvfs_test/usvfs_basic_test.cpp + usvfs_test/usvfs_basic_test.h + usvfs_test/usvfs_test_base.cpp + usvfs_test/usvfs_test_base.h + usvfs_test/usvfs_test.cpp +) +usvfs_set_test_properties(usvfs_test FOLDER usvfs_test_runner) +usvfs_target_link_usvfs(usvfs_test) +target_link_libraries(usvfs_test PRIVATE test_utils) + +# actual test executable +add_executable(usvfs_test_runner usvfs_test_runner.cpp) +usvfs_set_test_properties(usvfs_test_runner FOLDER usvfs_test_runner) +target_link_libraries(usvfs_test_runner + PRIVATE test_utils GTest::gtest GTest::gtest_main) +add_dependencies(usvfs_test_runner usvfs_test test_file_operations) diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_file_operations.cpp b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_file_operations.cpp new file mode 100644 index 0000000..2135df3 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_file_operations.cpp @@ -0,0 +1,496 @@ +#include +#include + +#include +#include +#include + +#include "test_ntapi.h" +#include "test_w32api.h" +#include + +#define WIN32_LEAN_AND_MEAN +#include + +using usvfs::shared::CodePage; +using usvfs::shared::string_cast; + +void print_usage(const char* myname) +{ + using namespace std; + fprintf(stderr, "usage: %s [] [] ...\n", myname); + fprintf(stderr, + "options and commands are parsed and executed in the order they appear.\n"); + fprintf(stderr, "\nsupported commands:\n"); + fprintf( + stderr, + " -list : lists the given directory and outputs the results.\n"); + fprintf(stderr, " -listcontents : lists the given directory, reading all files " + "and outputs the results.\n"); + fprintf(stderr, + " -read : reads the given file and outputs the results.\n"); + fprintf(stderr, " -overwrite : overwrites the file at the given path " + "with the given line (creating directories if in recursive mode).\n"); + fprintf(stderr, " -deleteoverwrite : shorthand for -delete " + "-overwrite \n"); + fprintf(stderr, + " -rewrite : rewrites the file at the given path with the " + "given line (fails if file doesn't exist; uses read/write access).\n"); + fprintf(stderr, + " -touch : updates last write timestamp of specified file.\n"); + fprintf(stderr, " -touchw : updates last write timestamp using full " + "write permissions.\n"); + fprintf(stderr, " -delete : deletes the given file.\n"); + fprintf(stderr, " -copy : copies the given file.\n"); + fprintf(stderr, " -copyover : copies the given file (replacing existing " + "destination).\n"); + fprintf(stderr, " -rename : renames the given file.\n"); + fprintf(stderr, " -renameover : renames the given file (replacing " + "existing destination).\n"); + fprintf(stderr, " -deleterename : shorthand for -delete -rename " + " .\n"); + fprintf(stderr, + " -move : moves the given file (not supported by ntapi).\n"); + fprintf(stderr, " -moveover : moves the given file (replacing existing " + "destination; not supported by ntapi).\n"); + fprintf( + stderr, + " -deletemove : shorthand for -delete -move .\n"); + fprintf(stderr, " -debug : shows a message box and wait for a debugger " + "to connect.\n"); + fprintf(stderr, "\nsupported options:\n"); + fprintf(stderr, " -out : file to log output to (use \"-\" for the " + "stdout; otherwise path to output should exist).\n"); + fprintf(stderr, " -out+ : similar to -out but appends the file instead " + "of overwriting it.\n"); + fprintf(stderr, " -cout : similar to -out but does not log PID and " + "other info which may change between runs.\n"); + fprintf(stderr, " -cout+ : similar to -cout but appends the file " + "instead of overwriting it.\n"); + fprintf(stderr, " -r : recursively list/create directories.\n"); + fprintf(stderr, + " -r- : don't recursively list/create directories.\n"); + fprintf(stderr, " -basedir : any paths under the basedir will outputed in " + "a relative manner (default is current directory).\n"); + fprintf(stderr, + " -w32api : use regular Win32 API for file access (default).\n"); + fprintf(stderr, + " -ntapi : use lower level ntdll functions for file access.\n"); +} + +class CommandExecuter +{ +public: + CommandExecuter() : m_output(stdout), m_api(&w32api) + { + set_basedir(TestFileSystem::current_directory().string().c_str()); + } + + ~CommandExecuter() + { + if (m_output && m_output != stdout) + fclose(m_output); + } + + FILE* output() const { return m_output; } + + bool file_output() const { return m_output != stdout; } + + // Options: + + void set_output(const char* output_file, bool clean, bool append, const char* cmdline) + { + if (m_output && m_output != stdout) { + fprintf(m_output, "#< Output log closed.\n", output_file); + fclose(m_output); + } + + m_cleanoutput = clean; + m_output = nullptr; + errno_t err = fopen_s(&m_output, output_file, append ? "at" : "wt"); + if (err || !m_output) + test::throw_testWinFuncFailed("fopen_s", output_file, err); + else { + if (m_cleanoutput) + fprintf(m_output, "#> Output log openned for: %s\n", + clean_cmdline_heuristic(cmdline).c_str()); + else + fprintf(m_output, "#> Output log openned for (pid %d): %s\n", + GetCurrentProcessId(), cmdline); + w32api.set_output(m_output, m_cleanoutput); + ntapi.set_output(m_output, m_cleanoutput); + } + } + + bool cleanoutput() const { return m_cleanoutput; } + + void set_recursive(bool recursive) { m_recursive = recursive; } + + void set_basedir(const char* basedir) + { + w32api.set_basepath(basedir); + ntapi.set_basepath(basedir); + } + + void set_ntapi(bool enable) + { + if (enable) + m_api = &ntapi; + else + m_api = &w32api; + } + + // Commands: + + void list(const char* dir, bool read_files) + { + if (debug_pending()) + __debugbreak(); + + list_impl(m_api->real_path(dir), read_files); + } + + void read(const char* path) + { + if (debug_pending()) + __debugbreak(); + + m_api->read_file(m_api->real_path(path)); + } + + void overwrite(const char* path, const char* value) + { + if (debug_pending()) + __debugbreak(); + + const auto& real = real_path_create_if_necessary(path); + + m_api->write_file(real, value, strlen(value), true, + TestFileSystem::write_mode::overwrite); + } + + void rewrite(const char* path, const char* value) + { + if (debug_pending()) + __debugbreak(); + + const auto& real = real_path_create_if_necessary(path); + + // Use read/write access when rewriting to "simulate" the harder case where it is + // not known if the file is going to actually be changed + m_api->write_file(real, value, strlen(value), false, + TestFileSystem::write_mode::manual_truncate, true); + m_api->write_file(real, "\r\n", 2, false, TestFileSystem::write_mode::append); + } + + void touch(const char* path, bool full_write_access) + { + if (debug_pending()) + __debugbreak(); + + const auto& real = real_path_create_if_necessary(path); + + m_api->touch_file(real, full_write_access); + } + + void deletef(const char* path) + { + if (debug_pending()) + __debugbreak(); + + m_api->delete_file(m_api->real_path(path)); + } + + void rename(const char* source, const char* destination, bool replace_existing) + { + if (debug_pending()) + __debugbreak(); + + m_api->copy_file(m_api->real_path(source), m_api->real_path(destination), + replace_existing); + } + + void rename(const char* source, const char* destination, bool replace_existing, + bool allow_copy) + { + if (debug_pending()) + __debugbreak(); + + m_api->rename_file(m_api->real_path(source), m_api->real_path(destination), + replace_existing, allow_copy); + } + + void debug() { m_debug_pending = true; } + + bool debug_pending() + { + if (!m_debug_pending) + return false; + m_debug_pending = false; + if (!IsDebuggerPresent()) + MessageBoxA(NULL, "Connect a debugger and press OK to trigger a breakpoint", + "DEBUG", 0); + return IsDebuggerPresent(); + } + + // Traversal: + + void list_impl(TestFileSystem::path real, bool read_files) + { + std::vector recurse; + { + auto files = m_api->list_directory(real); + fprintf(m_output, ">> Listing directory {%s}:\n", + m_api->relative_path(real).string().c_str()); + for (auto f : files) { + if (f.is_dir()) { + fprintf(m_output, "[%s] DIR (attributes 0x%x)\n", + string_cast(f.name, CodePage::UTF8).c_str(), + f.attributes); + if (m_recursive && f.name != L"." && f.name != L"..") + recurse.push_back(real / f.name); + } else { + fprintf(m_output, "[%s] FILE (attributes 0x%x, %lld bytes)\n", + string_cast(f.name, CodePage::UTF8).c_str(), + f.attributes, f.size); + if (read_files) + m_api->read_file(real / f.name); + } + } + } + for (auto r : recurse) + list_impl(r, read_files); + } + +private: + TestFileSystem::path real_path_create_if_necessary(const char* path) + { + auto real = m_api->real_path(path); + if (m_recursive) + try { + m_api->create_path(real.parent_path()); + } catch (const std::exception& e) { + throw std::runtime_error( + std::format("Failed to create_path [{}] : {}", + m_api->relative_path(real.parent_path()).string(), e.what())); + } + return std::move(real); + } + + std::string clean_cmdline_arg(const char* arg_start, const char* arg_end) + { + if (arg_start == arg_end) + return std::string(); + bool quoted = *arg_start == '\"' && *(arg_end - 1) == '\"'; + const char* last_sep = arg_end; + while (last_sep != arg_start && *last_sep != '\\') + --last_sep; + if (arg_end - arg_start < (quoted ? 5 : 3) || arg_start[0] == '-' || + arg_start[quoted ? 2 : 1] != ':' || last_sep == arg_start) + return std::string(arg_start, arg_end); + std::string res = quoted ? "\"" : ""; + res.append(last_sep + 1, arg_end); + return res; + } + + std::string clean_cmdline_heuristic(const char* cmdline) + { + std::string res; + bool first = true; + while (*cmdline) { + const char* end = strchr(cmdline, ' '); + if (!end) + end = cmdline + strlen(cmdline); + if (first) + first = false; + else + res.push_back(' '); + res += clean_cmdline_arg(cmdline, end); + cmdline = end; + while (*cmdline == ' ') + ++cmdline; + } + return res; + } + + FILE* m_output; + bool m_cleanoutput = false; + bool m_recursive = false; + bool m_debug_pending = false; + + TestFileSystem* m_api; + static TestW32Api w32api; + static TestNtApi ntapi; +}; + +// static +TestW32Api CommandExecuter::w32api(stdout); +TestNtApi CommandExecuter::ntapi(stdout); + +class abort_invalid_argument : std::exception +{}; + +bool verify_args_exist(const char* flag, int params, int index, int count) +{ + if (index + params >= count) { + fprintf(stderr, "ERROR: %s requires %d arguments\n", flag, params); + throw abort_invalid_argument(); + } + return true; +} + +const char* UntouchedCommandLineArguments() +{ + const char* cmd = GetCommandLineA(); + for (; *cmd && *cmd != ' '; ++cmd) { + if (*cmd == '"') { + int escaped = 0; + for (++cmd; *cmd && (*cmd != '"' || escaped % 2 != 0); ++cmd) + escaped = *cmd == '\\' ? escaped + 1 : 0; + } + } + while (*cmd == ' ') + ++cmd; + return cmd; +} + +int main(int argc, char* argv[]) +{ + bool found_commands = false; + CommandExecuter executer; + + TestFileSystem::path exe_path = argv[0]; + std::string exename = exe_path.filename().string(); + std::string cmdline = exename + " " + UntouchedCommandLineArguments(); + fprintf(stdout, "#> process %d started with commandline: %s\n", GetCurrentProcessId(), + cmdline.c_str()); + + for (int ai = 1; ai < argc; ++ai) { + try { + SetLastError(0); + + // options: + if (strcmp(argv[ai], "-out") == 0 && verify_args_exist("-out", 1, ai, argc) || + strcmp(argv[ai], "-out+") == 0 && verify_args_exist("-out+", 1, ai, argc) || + strcmp(argv[ai], "-cout") == 0 && verify_args_exist("-cout", 1, ai, argc) || + strcmp(argv[ai], "-cout+") == 0 && verify_args_exist("-cout+", 1, ai, argc)) { + bool clean = argv[ai][1] == 'c'; + bool append = argv[ai][clean ? 5 : 4] == '+'; + executer.set_output(argv[++ai], clean, append, cmdline.c_str()); + } else if (strcmp(argv[ai], "-r") == 0) + executer.set_recursive(true); + else if (strcmp(argv[ai], "-r-") == 0) + executer.set_recursive(false); + else if (strcmp(argv[ai], "-basedir") == 0 && + verify_args_exist("-basedir", 1, ai, argc)) { + executer.set_basedir(argv[++ai]); + } else if (strcmp(argv[ai], "-w32api") == 0) + executer.set_ntapi(false); + else if (strcmp(argv[ai], "-ntapi") == 0) + executer.set_ntapi(true); + // commands: + else if ((strcmp(argv[ai], "-list") == 0 || + strcmp(argv[ai], "-listcontents") == 0) && + verify_args_exist("-list", 1, ai, argc)) { + bool contents = strcmp(argv[ai], "-listcontents") == 0; + executer.list(argv[++ai], contents); + found_commands = true; + } else if (strcmp(argv[ai], "-read") == 0 && + verify_args_exist("-read", 1, ai, argc)) { + executer.read(argv[++ai]); + found_commands = true; + } else if (strcmp(argv[ai], "-overwrite") == 0 && + verify_args_exist("-overwrite", 2, ai, argc) || + strcmp(argv[ai], "-deleteoverwrite") == 0 && + verify_args_exist("-deleteoverwrite", 2, ai, argc)) { + if (argv[ai][1] == 'd') + executer.deletef(argv[ai + 1]); + executer.overwrite(argv[ai + 1], argv[ai + 2]); + ++ ++ai; + found_commands = true; + } else if (strcmp(argv[ai], "-rewrite") == 0 && + verify_args_exist("-rewrite", 2, ai, argc)) { + executer.rewrite(argv[ai + 1], argv[ai + 2]); + ++ ++ai; + found_commands = true; + } else if (strcmp(argv[ai], "-touch") == 0 && + verify_args_exist("-touch", 1, ai, argc) || + strcmp(argv[ai], "-touchw") == 0 && + verify_args_exist("-touchw", 1, ai, argc)) { + bool write_access = argv[ai][6] == 'w'; + executer.touch(argv[++ai], write_access); + found_commands = true; + } else if (strcmp(argv[ai], "-delete") == 0 && + verify_args_exist("-delete", 1, ai, argc)) { + executer.deletef(argv[++ai]); + found_commands = true; + } else if (strcmp(argv[ai], "-copy") == 0 && + verify_args_exist("-copy", 2, ai, argc) || + strcmp(argv[ai], "-copyover") == 0 && + verify_args_exist("-copyover", 2, ai, argc)) { + bool over = argv[ai][5] == 'o'; + executer.rename(argv[ai + 1], argv[ai + 2], over); + ++ ++ai; + found_commands = true; + } else if (strcmp(argv[ai], "-rename") == 0 && + verify_args_exist("-rename", 2, ai, argc) || + strcmp(argv[ai], "-renameover") == 0 && + verify_args_exist("-renameover", 2, ai, argc) || + strcmp(argv[ai], "-deleterename") == 0 && + verify_args_exist("-deleterename", 2, ai, argc) || + strcmp(argv[ai], "-move") == 0 && + verify_args_exist("-move", 2, ai, argc) || + strcmp(argv[ai], "-moveover") == 0 && + verify_args_exist("-moveover", 2, ai, argc) || + strcmp(argv[ai], "-deletemove") == 0 && + verify_args_exist("-deletemove", 2, ai, argc)) { + bool del = argv[ai][1] == 'd'; + bool move = argv[ai][del ? 7 : 1] == 'm'; + bool over = !del && argv[ai][move ? 5 : 7] == 'o'; + if (del) + executer.deletef(argv[ai + 2]); + executer.rename(argv[ai + 1], argv[ai + 2], over, move); + ++ ++ai; + found_commands = true; + } else if (strcmp(argv[ai], "-debug") == 0) { + executer.debug(); + } else { + if (executer.file_output()) + fprintf(executer.output(), "ERROR: invalid argument {%s}\n", argv[ai]); + fprintf(stderr, "ERROR: invalid argument {%s}\n", argv[ai]); + return 1; + } + } catch (const abort_invalid_argument&) { + return 1; + } +#if 1 // just a convient way to not catch exception when debugging + catch (const std::exception& e) { + if (executer.file_output()) + fprintf(executer.output(), "ERROR: %hs\n", e.what()); + fprintf(stderr, "ERROR: %hs\n", e.what()); + return 1; + } catch (...) { + if (executer.file_output()) + fprintf(executer.output(), "ERROR: unknown exception"); + fprintf(stderr, "ERROR: unknown exception\n"); + return 1; + } +#endif + } + + if (!found_commands) { + print_usage(exename.c_str()); + return 2; + } + + if (executer.file_output()) + if (executer.cleanoutput()) + fprintf(executer.output(), "#< %s ended properly.\n", exename.c_str()); + else + fprintf(executer.output(), "#< %s ended properly in process %d.\n", + exename.c_str(), GetCurrentProcessId()); + fprintf(stdout, "#< %s ended properly in process %d.\n", exename.c_str(), + GetCurrentProcessId()); + + return 0; +} diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.cpp b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.cpp new file mode 100644 index 0000000..dcfd5a1 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.cpp @@ -0,0 +1,159 @@ + +#include "test_filesystem.h" +#include +#define WIN32_LEAN_AND_MEAN +#include + +using test::throw_testWinFuncFailed; + +bool TestFileSystem::FileInformation::is_dir() const +{ + return (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; +} + +bool TestFileSystem::FileInformation::is_file() const +{ + return (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; +} + +TestFileSystem::TestFileSystem(FILE* output) : m_output(output) {} + +TestFileSystem::path TestFileSystem::current_directory() +{ + DWORD res = GetCurrentDirectoryW(0, NULL); + if (!res) + throw_testWinFuncFailed("GetCurrentDirectory", res); + std::wstring buf(res + 1, '\0'); + res = GetCurrentDirectoryW(buf.length(), &buf[0]); + if (!res || res >= buf.length()) + throw_testWinFuncFailed("GetCurrentDirectory", res); + buf.resize(res); + return buf; +} + +TestFileSystem::path TestFileSystem::relative_path(path full_path) +{ + return test::path_as_relative(m_basepath, full_path); +} + +// static +const char* TestFileSystem::write_operation_name(write_mode mode) +{ + switch (mode) { + case write_mode::manual_truncate: + return "Writing file (by open & truncate)"; + case write_mode::truncate: + return "Truncating file"; + case write_mode::create: + return "Creating file"; + case write_mode::overwrite: + return "Overwriting file"; + case write_mode::opencreate: + return "Opening/Creating file"; + case write_mode::append: + return "Appending file"; + } + return "Unknown write operation?!"; +} + +// static +const char* TestFileSystem::rename_operation_name(bool replace_existing, + bool allow_copy) +{ + if (allow_copy) + return replace_existing ? "Moving file over" : "Moving file"; + else + return replace_existing ? "Renaming file over" : "Renaming file"; +} + +void TestFileSystem::print_operation(const char* operation, const path& target) +{ + if (m_output) + fprintf(m_output, "# (%s) %s {%s}\n", id(), operation, + relative_path(target).string().c_str()); +} + +void TestFileSystem::print_operation(const char* operation, const path& source, + const path& target) +{ + if (m_output) + fprintf(m_output, "# (%s) %s {%s} {%s}\n", id(), operation, + relative_path(source).string().c_str(), + relative_path(target).string().c_str()); +} + +static inline void print_op_with_result(FILE* output, const char* prefix, + const char* operation, const uint32_t* result, + DWORD* last_error, const char* opt_arg) +{ + if (output) { + fprintf(output, "%s%s", prefix, operation); + if (opt_arg) + fprintf(output, " %s", opt_arg); + if (result) + fprintf(output, " returned %u (0x%x)", *result, *result); + if (last_error) + fprintf(output, " last error %u (0x%x)", *last_error, *last_error); + fprintf(output, "\n"); + } +} + +void TestFileSystem::print_result(const char* operation, uint32_t result, + bool with_last_error, const char* opt_arg, + bool hide_result) +{ + if (m_output) { + DWORD last_error = GetLastError(); + std::string prefix = "# ("; + prefix += id(); + prefix += ") "; + print_op_with_result(m_output, prefix.c_str(), operation, + hide_result ? nullptr : &result, + with_last_error ? &last_error : nullptr, opt_arg); + SetLastError(last_error); + } +} + +void TestFileSystem::print_error(const char* operation, uint32_t result, + bool with_last_error, const char* opt_arg) +{ + DWORD last_error = with_last_error ? GetLastError() : 0; + print_op_with_result(stderr, "ERROR: ", operation, &result, + with_last_error ? &last_error : nullptr, opt_arg); + if (m_output && m_output != stdout) + print_op_with_result(m_output, "ERROR: ", operation, &result, + with_last_error ? &last_error : nullptr, opt_arg); +} + +void TestFileSystem::print_write_success(const void* data, std::size_t size, + std::size_t written) +{ + if (m_output) { + fprintf(m_output, "# Successfully written %u bytes ", + static_cast(written)); + // heuristics to print nicer one liners: + if (size == 1 && reinterpret_cast(data)[0] == '\n' || + size == 2 && reinterpret_cast(data)[0] == '\r' && + reinterpret_cast(data)[1] == '\n') + fprintf(m_output, ""); + else { + fprintf(m_output, "{"); + if (size && reinterpret_cast(data)[size - 1] == '\n') + --size; + if (data) + fwrite(data, 1, size, m_output); + else if (size) + fwrite("NULL?!", 1, 6, m_output); + fprintf(m_output, "}"); + } + fprintf(output(), "\n"); + } +} + +uint32_t TestFileSystem::clean_attributes(uint32_t attr) +{ + if (m_cleanoutput) + return attr & ~(FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); + else + return attr; +} diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.h b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.h new file mode 100644 index 0000000..3a5f53b --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.h @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include +#include + +class TestFileSystem +{ +public: + static constexpr auto FILE_CONTENTS_PRINT_PREFIX = "== "; + + typedef std::filesystem::path path; + typedef std::FILE FILE; + + static path current_directory(); + + TestFileSystem(FILE* output); + + void set_output(FILE* output, bool clean) + { + m_output = output; + m_cleanoutput = clean; + } + + // base path used to trim outputs (which is important so we can compare tests ran at + // different base paths) + void set_basepath(const char* path) { m_basepath = real_path(path); } + + // returns the path relative to the base path + path relative_path(path full_path); + + virtual const char* id() = 0; + + virtual path real_path(const char* abs_or_rel_path) = 0; + + struct FileInformation + { + std::wstring name; + uint32_t attributes; + uint64_t size; + + FileInformation(const std::wstring& iname, uint32_t iattributes, uint64_t isize) + : name(iname), attributes(iattributes), size(isize) + {} + + bool is_dir() const; + bool is_file() const; + }; + typedef std::vector FileInfoList; + + virtual FileInfoList list_directory(const path& directory_path) = 0; + + virtual void create_path(const path& directory_path) = 0; + + virtual void read_file(const path& file_path) = 0; + + enum class write_mode + { + manual_truncate, + truncate, + create, + overwrite, + opencreate, + append + }; + virtual void write_file(const path& file_path, const void* data, std::size_t size, + bool add_new_line, write_mode mode, + bool rw_access = false) = 0; + + virtual void touch_file(const path& file_path, bool full_write_access = false) = 0; + + virtual void delete_file(const path& file_path) = 0; + + virtual void copy_file(const path& source_path, const path& destination_path, + bool replace_existing) = 0; + + virtual void rename_file(const path& source_path, const path& destination_path, + bool replace_existing, bool allow_copy) = 0; + +protected: + FILE* output() { return m_output; } + bool cleanoutput() const { return m_cleanoutput; } + static const char* write_operation_name(write_mode mode); + static const char* rename_operation_name(bool replace_existing, bool allow_copy); + +public: // mainly for derived class (but also used by helper classes like SafeHandle so + // public) + void print_operation(const char* operation, const path& target); + void print_operation(const char* operation, const path& source, const path& target); + void print_result(const char* operation, uint32_t result, + bool with_last_error = false, const char* opt_arg = nullptr, + bool hide_result = false); + void print_error(const char* operation, uint32_t result, bool with_last_error = false, + const char* opt_arg = nullptr); + void print_write_success(const void* data, std::size_t size, std::size_t written); + + uint32_t clean_attributes(uint32_t attr); + +private: + FILE* m_output; + bool m_cleanoutput = false; + path m_basepath; +}; diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.cpp b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.cpp new file mode 100644 index 0000000..34966b5 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.cpp @@ -0,0 +1,488 @@ + +#include "test_ntapi.h" +#include +#include +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +#include "test_ntdll_declarations.h" + +class TestNtApi::SafeHandle +{ +public: + SafeHandle(TestFileSystem* tfs, HANDLE handle = NULL) : m_handle(handle), m_tfs(tfs) + {} + SafeHandle(const SafeHandle&) = delete; + SafeHandle(SafeHandle&& other) : m_handle(other.m_handle), m_tfs(other.m_tfs) + { + other.m_handle = nullptr; + } + + operator HANDLE() { return m_handle; } + operator PHANDLE() { return &m_handle; } + + bool valid() const { return m_handle != NULL; } + + ~SafeHandle() + { + if (m_handle) { + NTSTATUS status = NtClose(m_handle); + if (m_tfs) + m_tfs->print_result("NtClose", status); + if (!NT_SUCCESS(status)) + if (m_tfs) + m_tfs->print_error("NtClose", status); + else + std::fprintf(stderr, "NtClose failed : 0x%lx", status); + m_handle = NULL; + } + } + +private: + HANDLE m_handle; + TestFileSystem* m_tfs; +}; + +const char* TestNtApi::id() +{ + return "Nt"; +} + +TestNtApi::path TestNtApi::real_path(const char* abs_or_rel_path) +{ + if (!abs_or_rel_path || !abs_or_rel_path[0]) + return path(); + + static constexpr char nt_path_prefix[] = "\\??\\"; + static constexpr wchar_t nt_path_prefix_w[] = L"\\??\\"; + + bool path_dos = strncmp(abs_or_rel_path, nt_path_prefix, strlen(nt_path_prefix)) == 0; + bool path_has_drive = abs_or_rel_path[1] == ':'; + bool path_unc = abs_or_rel_path[0] == '\\' && abs_or_rel_path[1] == '\\'; + bool path_absolute = path_has_drive || abs_or_rel_path[0] == '\\'; + + path result; + if (!path_dos) { + if (!path_unc) + result.assign(nt_path_prefix_w); + if (!path_absolute) + result /= current_directory(); + else if (!path_has_drive && !path_unc) + // if "absolute" path but without a drive letter (i.e. \windows) + // the take the drive from the current directory: (i.e. "C:") + result /= current_directory().root_name(); + } + + int result_size = 0; + for (auto r : result) + ++result_size; + + // now append abs_or_rel_path, handling ".." and "." properly: + path arp{abs_or_rel_path}; + int base_size = path_unc ? 3 : 4; + for (auto p : arp) { + if (p == "..") { + if (result_size > base_size) { // refuse to remove top level element (i.e. + // \??\C:\ which is 4 elements) + result.remove_filename(); + --result_size; + } + } else if (!p.empty() && p != ".") { + result /= p; + ++result_size; + } + } + + return result; +} + +TestNtApi::SafeHandle TestNtApi::open_directory(const path& directory_path, bool create, + bool allow_non_existence, long* pstatus) +{ + print_operation(create ? "Creating directory" : "Openning directory", directory_path); + + UNICODE_STRING unicode_path; + RtlInitUnicodeString(&unicode_path, directory_path.c_str()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &unicode_path, OBJ_CASE_INSENSITIVE, NULL, + NULL); + + SafeHandle dir(this); + IO_STATUS_BLOCK iosb; + NTSTATUS status = NtCreateFile( + dir, FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE, &attributes, &iosb, NULL, + FILE_ATTRIBUTE_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + create ? FILE_OPEN_IF : FILE_OPEN, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); + + print_result("NtCreateFile", status); + + if (pstatus) + *pstatus = status; + if ((status == STATUS_OBJECT_NAME_NOT_FOUND || + status == STATUS_OBJECT_PATH_NOT_FOUND) && + allow_non_existence) + return NULL; + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtCreateFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtCreateFile", "bad iosb.Status", iosb.Status); + + return dir; +} + +TestFileSystem::FileInfoList TestNtApi::list_directory(const path& directory_path) +{ + SafeHandle dir = open_directory(directory_path, false); + + print_operation("Querying directory", directory_path); + + FileInfoList files; + while (true) { + char buf[4096]{0}; + IO_STATUS_BLOCK iosb; + + NTSTATUS status = + NtQueryDirectoryFile(dir, NULL, NULL, NULL, &iosb, buf, sizeof(buf), + MyFileBothDirectoryInformation, FALSE, NULL, FALSE); + print_result("NtQueryDirectoryFile", status); + + if (status == STATUS_NO_MORE_FILES) + break; + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtQueryDirectoryFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtQueryDirectoryFile", "bad iosb.Status", iosb.Status); + if (iosb.Information == 0) // This shouldn't happend unless the filename (not full + // path) is larger then sizeof(buf) + throw test::FuncFailed("NtQueryDirectoryFile", "buffer too small", + iosb.Information); + + PFILE_BOTH_DIR_INFORMATION info = reinterpret_cast(buf); + while (true) { + files.push_back(FileInformation( + std::wstring(info->FileName, + info->FileNameLength / sizeof(info->FileName[0])), + clean_attributes(info->FileAttributes), info->EndOfFile.QuadPart)); + if (info->NextEntryOffset) + info = reinterpret_cast( + reinterpret_cast(info) + info->NextEntryOffset); + else + break; + } + } + + return files; +} + +void TestNtApi::create_path(const path& directory_path) +{ + // sanity and guaranteed recursion end: + if (!directory_path.has_relative_path()) + throw std::runtime_error("Refusing to create non-existing top level path"); + + // if directory already exists all is good + NTSTATUS status; + if (open_directory(directory_path, false, true, &status).valid()) + return; + + if (status != STATUS_OBJECT_NAME_NOT_FOUND) // STATUS_OBJECT_NAME_NOT_FOUND means + // parent directory already exists + create_path(directory_path + .parent_path()); // otherwise create parent directory (recursively) + + open_directory(directory_path, true); +} + +void TestNtApi::read_file(const path& file_path) +{ + print_operation("Reading file", file_path); + + UNICODE_STRING unicode_path; + RtlInitUnicodeString(&unicode_path, file_path.c_str()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &unicode_path, OBJ_CASE_INSENSITIVE, NULL, + NULL); + + SafeHandle file(this); + IO_STATUS_BLOCK iosb; + NTSTATUS status = NtOpenFile(file, GENERIC_READ | SYNCHRONIZE, &attributes, &iosb, + FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT); + print_result("NtOpenFile", status); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtOpenFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtOpenFile", "bad iosb.Status", iosb.Status); + + uint32_t total = 0; + bool ends_with_newline = true; + bool pending_prefix = true; + while (true) { + char buf[4096]; + + memset(&iosb, 0, sizeof(iosb)); + status = NtReadFile(file, NULL, NULL, NULL, &iosb, buf, sizeof(buf), NULL, NULL); + print_result("NtReadFile", status); + if (status == STATUS_END_OF_FILE) + break; + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtReadFile", status); + + total += iosb.Information; + char* begin = buf; + char* end = begin + iosb.Information; + while (begin != end) { + if (pending_prefix) { + if (output()) + fwrite(FILE_CONTENTS_PRINT_PREFIX, 1, strlen(FILE_CONTENTS_PRINT_PREFIX), + output()); + pending_prefix = false; + } + bool skip_newline = false; + char* print_end = reinterpret_cast(std::memchr(begin, '\n', end - begin)); + if (print_end) { + pending_prefix = true; + if (print_end > begin && *(print_end - 1) == '\r') { + // convert \r\n => \n: + *(print_end - 1) = '\n'; + skip_newline = true; + } else // only a '\n' so just print it + ++print_end; + } else { + print_end = end; + if (print_end > begin && *(print_end - 1) == '\r') { + // buffer ends with \r so skip it under the hope it will be followed with a \n + --print_end; + skip_newline = true; + } + } + if (output()) + fwrite(begin, 1, print_end - begin, output()); + ends_with_newline = print_end > begin && *(print_end - 1) == '\n'; + begin = print_end; + if (skip_newline) + ++begin; + } + if (output() && !ends_with_newline) { + fwrite("\n", 1, 1, output()); + ends_with_newline = true; + } + } + if (output()) { + fprintf(output(), "# Successfully read %u bytes.\n", total); + } +} + +void TestNtApi::write_file(const path& file_path, const void* data, std::size_t size, + bool add_new_line, write_mode mode, bool rw_access) +{ + print_operation(write_operation_name(mode), file_path); + + UNICODE_STRING unicode_path; + RtlInitUnicodeString(&unicode_path, file_path.c_str()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &unicode_path, OBJ_CASE_INSENSITIVE, NULL, + NULL); + + ACCESS_MASK access = GENERIC_WRITE | SYNCHRONIZE; + ULONG disposition = FILE_OPEN; + switch (mode) { + case write_mode::truncate: + disposition = FILE_OVERWRITE; + break; + case write_mode::create: + disposition = FILE_CREATE; + break; + case write_mode::overwrite: + disposition = FILE_SUPERSEDE; + break; + case write_mode::opencreate: + disposition = FILE_OPEN_IF; + break; + case write_mode::append: + disposition = FILE_OPEN_IF; + access = FILE_APPEND_DATA | SYNCHRONIZE; + break; + } + if (rw_access) + access |= GENERIC_READ; + + SafeHandle file(this); + IO_STATUS_BLOCK iosb; + NTSTATUS status = + NtCreateFile(file, access, &attributes, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, 0, + disposition, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); + print_result("NtCreateFile", status); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtCreateFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtCreateFile", "bad iosb.Status", iosb.Status); + + if (mode == write_mode::manual_truncate) { + FILE_END_OF_FILE_INFORMATION eofinfo{0}; + status = NtSetInformationFile(file, &iosb, &eofinfo, sizeof(eofinfo), + MyFileEndOfFileInformation); + print_result("NtSetInformationFile", status, false, "EOF"); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtSetInformationFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtSetInformationFile", "bad iosb.Status", iosb.Status); + } + + // finally write the data: + size_t total = 0; + + if (data) { + status = NtWriteFile(file, NULL, NULL, NULL, &iosb, const_cast(data), + static_cast(size), NULL, NULL); + print_result("NtWriteFile", status); + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtWriteFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtWriteFile", "bad iosb.Status", iosb.Status); + total += iosb.Information; + + if (add_new_line) { + char buffer[] = "\r\n"; + status = NtWriteFile(file, NULL, NULL, NULL, &iosb, buffer, 2, NULL, NULL); + print_result("NtWriteFile", status); + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtWriteFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtWriteFile", "bad iosb.Status", iosb.Status); + total += iosb.Information; + } + } + + print_write_success(data, size, total); +} + +void TestNtApi::touch_file(const path& file_path, bool full_write_access) +{ + print_operation("Touching file", file_path); + + SYSTEMTIME st; + GetSystemTime(&st); + FILETIME ft; + if (!SystemTimeToFileTime(&st, &ft)) + test::throw_testWinFuncFailed("SystemTimeToFileTime"); + + UNICODE_STRING unicode_path; + RtlInitUnicodeString(&unicode_path, file_path.c_str()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &unicode_path, OBJ_CASE_INSENSITIVE, NULL, + NULL); + + SafeHandle file(this); + IO_STATUS_BLOCK iosb; + auto share_all = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; + auto access = + (full_write_access ? GENERIC_WRITE : FILE_WRITE_ATTRIBUTES) | SYNCHRONIZE; + NTSTATUS status = + NtCreateFile(file, access, &attributes, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, + share_all, FILE_OPEN_IF, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); + print_result("NtCreateFile", status); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtCreateFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtCreateFile", "bad iosb.Status", iosb.Status); + + FILE_BASIC_INFORMATION basicinfo{0}; + basicinfo.LastWriteTime.LowPart = ft.dwLowDateTime; + basicinfo.LastWriteTime.HighPart = ft.dwHighDateTime; + status = NtSetInformationFile(file, &iosb, &basicinfo, sizeof(basicinfo), + MyFileBasicInformation); + print_result("NtSetInformationFile", status, false, "Basic"); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtSetInformationFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtSetInformationFile", "bad iosb.Status", iosb.Status); +} + +void TestNtApi::delete_file(const path& file_path) +{ + print_operation("Deleting file", file_path); + + UNICODE_STRING unicode_path; + RtlInitUnicodeString(&unicode_path, file_path.c_str()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &unicode_path, OBJ_CASE_INSENSITIVE, NULL, + NULL); + + NTSTATUS status = NtDeleteFile(&attributes); + print_result("NtCreateFile", status); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtDeleteFile", status); +} + +void TestNtApi::copy_file(const path& source_path, const path& destination_path, + bool replace_existing) +{ + throw test::FuncFailed("copy_file", "ntapi does not support file copy"); +} + +void TestNtApi::rename_file(const path& source_path, const path& destination_path, + bool replace_existing, bool allow_copy) +{ + if (allow_copy) + throw test::FuncFailed("rename_file", "ntapi does not support file move"); + + print_operation(rename_operation_name(replace_existing, allow_copy), source_path, + destination_path); + + UNICODE_STRING unicode_path; + RtlInitUnicodeString(&unicode_path, source_path.c_str()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &unicode_path, OBJ_CASE_INSENSITIVE, NULL, + NULL); + + SafeHandle file(this); + IO_STATUS_BLOCK iosb; + NTSTATUS status = NtCreateFile(file, FILE_READ_ATTRIBUTES | DELETE | SYNCHRONIZE, + &attributes, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); + print_result("NtCreateFile", status); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtCreateFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtCreateFile", "bad iosb.Status", iosb.Status); + + bool dest_full_path = source_path.parent_path() != destination_path.parent_path(); + std::wstring dest = dest_full_path ? destination_path : destination_path.filename(); + std::vector buf(sizeof(FILE_RENAME_INFORMATION) + + sizeof(wchar_t) * dest.length()); + FILE_RENAME_INFORMATION* rename = + reinterpret_cast(buf.data()); + rename->ReplaceIfExists = replace_existing ? TRUE : FALSE; + rename->FileNameLength = sizeof(wchar_t) * dest.length(); + memcpy(&rename->FileName[0], dest.data(), sizeof(wchar_t) * dest.length()); + + status = + NtSetInformationFile(file, &iosb, rename, buf.size(), MyFileRenameInformation); + print_result("NtSetInformationFile", status, false, + dest_full_path ? "rename full path" : "rename filename"); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtSetInformationFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtSetInformationFile", "bad iosb.Status", iosb.Status); +} diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.h b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.h new file mode 100644 index 0000000..fc6434e --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.h @@ -0,0 +1,38 @@ +#pragma once + +#include "test_filesystem.h" + +class TestNtApi : public TestFileSystem +{ +public: + TestNtApi(FILE* output) : TestFileSystem(output) {} + + path real_path(const char* abs_or_rel_path) override; + + FileInfoList list_directory(const path& directory_path) override; + + void create_path(const path& directory_path) override; + + void read_file(const path& file_path) override; + + void write_file(const path& file_path, const void* data, std::size_t size, + bool add_new_line, write_mode mode, bool rw_access = false) override; + + void touch_file(const path& file_path, bool full_write_access = false) override; + + void delete_file(const path& file_path) override; + + void copy_file(const path& source_path, const path& destination_path, + bool replace_existing) override; + + void rename_file(const path& source_path, const path& destination_path, + bool replace_existing, bool allow_copy) override; + + const char* id() override; + +private: + class SafeHandle; + + SafeHandle open_directory(const path& directory_path, bool create, + bool allow_non_existence = false, long* pstatus = nullptr); +}; diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntdll_declarations.h b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntdll_declarations.h new file mode 100644 index 0000000..e3799ac --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntdll_declarations.h @@ -0,0 +1,157 @@ +#pragma once + +#define STATUS_NO_MORE_FILES 0x80000006 +#define STATUS_END_OF_FILE 0xC0000011 +#define STATUS_OBJECT_NAME_NOT_FOUND 0xC0000034 +#define STATUS_OBJECT_PATH_NOT_FOUND 0xC000003A + +#define FILE_WRITE_TO_END_OF_FILE 0xffffffff +#define FILE_USE_FILE_POINTER_POSITION 0xfffffffe + +#define InitializeObjectAttributes(p, n, a, r, s) \ + { \ + (p)->Length = sizeof(OBJECT_ATTRIBUTES); \ + (p)->RootDirectory = r; \ + (p)->Attributes = a; \ + (p)->ObjectName = n; \ + (p)->SecurityDescriptor = s; \ + (p)->SecurityQualityOfService = NULL; \ + } + +// winternl.h of course defines a joke FILE_INFORMATION_CLASS (why?!) +// so added MY_ to avoid name collision but this is FILE_INFORMATION_CLASS + +typedef enum _MY_FILE_INFORMATION_CLASS +{ + MyFileDirectoryInformation = 1, + MyFileFullDirectoryInformation, + MyFileBothDirectoryInformation, + MyFileBasicInformation, + MyFileStandardInformation, + MyFileInternalInformation, + MyFileEaInformation, + MyFileAccessInformation, + MyFileNameInformation, + MyFileRenameInformation, + MyFileLinkInformation, + MyFileNamesInformation, + MyFileDispositionInformation, + MyFilePositionInformation, + MyFileFullEaInformation, + MyFileModeInformation, + MyFileAlignmentInformation, + MyFileAllInformation, + MyFileAllocationInformation, + MyFileEndOfFileInformation, + MyFileAlternateNameInformation, + MyFileStreamInformation, + MyFilePipeInformation, + MyFilePipeLocalInformation, + MyFilePipeRemoteInformation, + MyFileMailslotQueryInformation, + MyFileMailslotSetInformation, + MyFileCompressionInformation, + MyFileObjectIdInformation, + MyFileCompletionInformation, + MyFileMoveClusterInformation, + MyFileQuotaInformation, + MyFileReparsePointInformation, + MyFileNetworkOpenInformation, + MyFileAttributeTagInformation, + MyFileTrackingInformation, + MyFileIdBothDirectoryInformation, + MyFileIdFullDirectoryInformation, + MyFileValidDataLengthInformation, + MyFileShortNameInformation, + MyFileIoCompletionNotificationInformation, + MyFileIoStatusBlockRangeInformation, + MyFileIoPriorityHintInformation, + MyFileSfioReserveInformation, + MyFileSfioVolumeInformation, + MyFileHardLinkInformation, + MyFileProcessIdsUsingFileInformation, + MyFileNormalizedNameInformation, + MyFileNetworkPhysicalNameInformation, + MyFileIdGlobalTxDirectoryInformation, + MyFileIsRemoteDeviceInformation, + MyFileUnusedInformation, + MyFileNumaNodeInformation, + MyFileStandardLinkInformation, + MyFileRemoteProtocolInformation, + MyFileRenameInformationBypassAccessCheck, + MyFileLinkInformationBypassAccessCheck, + MyFileVolumeNameInformation, + MyFileIdInformation, + MyFileIdExtdDirectoryInformation, + MyFileReplaceCompletionInformation, + MyFileHardLinkFullIdInformation, + MyFileIdExtdBothDirectoryInformation, + MyFileDispositionInformationEx, + MyFileRenameInformationEx, + MyFileRenameInformationExBypassAccessCheck, + MyFileMaximumInformation +} MY_FILE_INFORMATION_CLASS, + *PMY_FILE_INFORMATION_CLASS; + +typedef struct _FILE_BOTH_DIR_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION; + +typedef struct _FILE_BASIC_INFORMATION +{ + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + ULONG FileAttributes; +} FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION; + +typedef struct _FILE_END_OF_FILE_INFORMATION +{ + LARGE_INTEGER EndOfFile; +} FILE_END_OF_FILE_INFORMATION, *PFILE_END_OF_FILE_INFORMATION; + +typedef struct _FILE_RENAME_INFORMATION +{ + BOOLEAN ReplaceIfExists; + HANDLE RootDirectory; + ULONG FileNameLength; + WCHAR FileName[1]; +} FILE_RENAME_INFORMATION, *PFILE_RENAME_INFORMATION; + +extern "C" __kernel_entry NTSTATUS NTAPI NtQueryDirectoryFile( + IN HANDLE FileHandle, IN HANDLE Event, IN PIO_APC_ROUTINE ApcRoutine, + IN PVOID ApcContext, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID FileInformation, + IN ULONG Length, IN MY_FILE_INFORMATION_CLASS FileInformationClass, + IN BOOLEAN ReturnSingleEntry, IN PUNICODE_STRING FileName, IN BOOLEAN RestartScan); + +extern "C" __kernel_entry NTSTATUS NTAPI +NtReadFile(IN HANDLE FileHandle, IN HANDLE Event, IN PIO_APC_ROUTINE ApcRoutine, + IN PVOID ApcContext, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID Buffer, + IN ULONG Length, IN PLARGE_INTEGER ByteOffset, IN PULONG Key); + +extern "C" __kernel_entry NTSTATUS NTAPI +NtWriteFile(IN HANDLE FileHandle, IN HANDLE Event, IN PIO_APC_ROUTINE ApcRoutine, + IN PVOID ApcContext, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PVOID Buffer, + IN ULONG Length, IN PLARGE_INTEGER ByteOffset, IN PULONG Key); + +extern "C" __kernel_entry NTSTATUS NTAPI NtSetInformationFile( + IN HANDLE FileHandle, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PVOID FileInformation, + IN ULONG Length, IN MY_FILE_INFORMATION_CLASS FileInformationClass); + +extern "C" __kernel_entry NTSTATUS NTAPI +NtDeleteFile(IN POBJECT_ATTRIBUTES ObjectAttributes); diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.cpp b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.cpp new file mode 100644 index 0000000..372a6f3 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.cpp @@ -0,0 +1,370 @@ + +#include "test_w32api.h" +#include +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include + +using test::throw_testWinFuncFailed; + +class TestW32Api::SafeHandle +{ +public: + SafeHandle(TestFileSystem* tfs, HANDLE handle = NULL) : m_handle(handle), m_tfs(tfs) + {} + SafeHandle(const SafeHandle&) = delete; + SafeHandle(SafeHandle&& other) : m_handle(other.m_handle), m_tfs(other.m_tfs) + { + other.m_handle = nullptr; + } + + operator HANDLE() { return m_handle; } + operator PHANDLE() { return &m_handle; } + uint32_t result_for_print() + { + return static_cast(reinterpret_cast(m_handle)); + } + + bool valid() const { return m_handle != INVALID_HANDLE_VALUE; } + + ~SafeHandle() + { + if (m_handle != INVALID_HANDLE_VALUE) { + BOOL res = CloseHandle(m_handle); + if (m_tfs) + m_tfs->print_result("CloseHandle", res, true); + if (!res) + if (m_tfs) + m_tfs->print_error("CloseHandle", res, true); + else + std::fprintf(stderr, "CloseHandle failed : %d", GetLastError()); + m_handle = NULL; + } + } + +private: + HANDLE m_handle; + TestFileSystem* m_tfs; +}; + +class TestW32Api::SafeFindHandle +{ +public: + SafeFindHandle(TestFileSystem* tfs, HANDLE handle = NULL) + : m_handle(handle), m_tfs(tfs) + {} + SafeFindHandle(const SafeFindHandle&) = delete; + SafeFindHandle(SafeFindHandle&& other) : m_handle(other.m_handle), m_tfs(other.m_tfs) + { + other.m_handle = nullptr; + } + + operator HANDLE() { return m_handle; } + operator PHANDLE() { return &m_handle; } + uint32_t result_for_print() + { + return static_cast(reinterpret_cast(m_handle)); + } + + bool valid() const { return m_handle != INVALID_HANDLE_VALUE; } + + ~SafeFindHandle() + { + if (m_handle != INVALID_HANDLE_VALUE) { + BOOL res = FindClose(m_handle); + if (m_tfs) + m_tfs->print_result("CloseHandle", res, true); + if (!res) + if (m_tfs) + m_tfs->print_error("CloseHandle", res, true); + else + std::fprintf(stderr, "CloseHandle failed : %d", GetLastError()); + m_handle = NULL; + } + } + +private: + HANDLE m_handle; + TestFileSystem* m_tfs; +}; + +const char* TestW32Api::id() +{ + return "W32"; +} + +TestW32Api::path TestW32Api::real_path(const char* abs_or_rel_path) +{ + if (!abs_or_rel_path || !abs_or_rel_path[0]) + return path(); + + char buf[1024]; + size_t res = GetFullPathNameA(abs_or_rel_path, _countof(buf), buf, NULL); + if (!res || res >= _countof(buf)) + throw_testWinFuncFailed("GetFullPathNameA", res); + return buf; +} + +TestFileSystem::FileInfoList TestW32Api::list_directory(const path& directory_path) +{ + print_operation("Querying directory", directory_path); + + WIN32_FIND_DATA fd; + SafeFindHandle find(this, FindFirstFileW((directory_path / L"*").c_str(), &fd)); + print_result("FindFirstFileW", 0, true, nullptr, true); + if (!find.valid()) + throw_testWinFuncFailed("FindFirstFileW"); + + FileInfoList files; + while (true) { + files.push_back( + FileInformation(fd.cFileName, clean_attributes(fd.dwFileAttributes), + fd.nFileSizeHigh * (MAXDWORD + 1) + fd.nFileSizeLow)); + BOOL res = FindNextFileW(find, &fd); + print_result("FindNextFileW", res, true); + if (!res) + break; + } + + return files; +} + +void TestW32Api::create_path(const path& directory_path) +{ + // sanity and guaranteed recursion end: + if (!directory_path.has_relative_path()) + throw std::runtime_error("Refusing to create non-existing top level path"); + + print_operation("Checking directory", directory_path); + + DWORD attr = GetFileAttributesW(directory_path.c_str()); + DWORD err = GetLastError(); + print_result("GetFileAttributesW", clean_attributes(attr), true); + if (attr != INVALID_FILE_ATTRIBUTES) { + if (attr & FILE_ATTRIBUTE_DIRECTORY) + return; // if directory already exists all is good + else + throw std::runtime_error("path exists but not a directory"); + } + if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND) + throw_testWinFuncFailed("GetFileAttributesW"); + + if (err != ERROR_FILE_NOT_FOUND) // ERROR_FILE_NOT_FOUND means parent directory + // already exists + create_path(directory_path + .parent_path()); // otherwise create parent directory (recursively) + + print_operation("Creating directory", directory_path); + + BOOL res = CreateDirectoryW(directory_path.c_str(), NULL); + print_result("CreateDirectoryW", res, true); + if (!res) + throw_testWinFuncFailed("CreateDirectoryW"); +} + +void TestW32Api::read_file(const path& file_path) +{ + print_operation("Reading file", file_path); + + SafeHandle file(this, CreateFileW(file_path.c_str(), GENERIC_READ, FILE_SHARE_READ, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)); + print_result("CreateFileW", 0, true, nullptr, true); + if (!file.valid()) + throw_testWinFuncFailed("CreateFileW"); + + uint32_t total = 0; + bool ends_with_newline = true; + bool pending_prefix = true; + while (true) { + char buf[4096]; + + DWORD read = 0; + BOOL res = ReadFile(file, buf, sizeof(buf), &read, NULL); + print_result("ReadFile", res, true); + if (!res) + throw_testWinFuncFailed("ReadFile"); + if (!read) // eof + break; + + total += read; + char* begin = buf; + char* end = begin + read; + while (begin != end) { + if (pending_prefix) { + if (output()) + fwrite(FILE_CONTENTS_PRINT_PREFIX, 1, strlen(FILE_CONTENTS_PRINT_PREFIX), + output()); + pending_prefix = false; + } + bool skip_newline = false; + char* print_end = reinterpret_cast(std::memchr(begin, '\n', end - begin)); + if (print_end) { + pending_prefix = true; + if (print_end > begin && *(print_end - 1) == '\r') { + // convert \r\n => \n: + *(print_end - 1) = '\n'; + skip_newline = true; + } else // only a '\n' so just print it + ++print_end; + } else { + print_end = end; + if (print_end > begin && *(print_end - 1) == '\r') { + // buffer ends with \r so skip it under the hope it will be followed with a \n + --print_end; + skip_newline = true; + } + } + if (output()) + fwrite(begin, 1, print_end - begin, output()); + ends_with_newline = print_end > begin && *(print_end - 1) == '\n'; + begin = print_end; + if (skip_newline) + ++begin; + } + if (output() && !ends_with_newline) { + fwrite("\n", 1, 1, output()); + ends_with_newline = true; + } + } + if (output()) { + fprintf(output(), "# Successfully read %u bytes.\n", total); + } +} + +void TestW32Api::write_file(const path& file_path, const void* data, std::size_t size, + bool add_new_line, write_mode mode, bool rw_access) +{ + print_operation(write_operation_name(mode), file_path); + + ACCESS_MASK access = GENERIC_WRITE; + DWORD disposition = OPEN_EXISTING; + switch (mode) { + case write_mode::truncate: + disposition = TRUNCATE_EXISTING; + break; + case write_mode::create: + disposition = CREATE_NEW; + break; + case write_mode::overwrite: + disposition = CREATE_ALWAYS; + break; + case write_mode::opencreate: + disposition = OPEN_ALWAYS; + break; + case write_mode::append: + disposition = OPEN_ALWAYS; + access = FILE_APPEND_DATA; + break; + } + if (rw_access) + access |= GENERIC_READ; + + SafeHandle file(this, CreateFile(file_path.c_str(), access, 0, NULL, disposition, + FILE_ATTRIBUTE_NORMAL, NULL)); + print_result("CreateFileW", 0, true, nullptr, true); + if (!file.valid()) + throw_testWinFuncFailed("CreateFile"); + + if (mode == write_mode::manual_truncate) { + BOOL res = SetEndOfFile(file); + print_result("SetEndOfFile", res, true); + if (!res) + throw_testWinFuncFailed("SetEndOfFile"); + } + + if (mode == write_mode::append) { + DWORD res = SetFilePointer(file, 0, NULL, FILE_END); + print_result("SetFilePointer(FILE_END)", res, true); + if (res == INVALID_SET_FILE_POINTER) + throw_testWinFuncFailed("SetEndOfFile"); + } + + size_t total = 0; + + if (data) { + // finally write the data: + DWORD written = 0; + BOOL res = WriteFile(file, data, static_cast(size), &written, NULL); + print_result("WriteFile", written, true); + if (!res) + throw_testWinFuncFailed("WriteFile"); + total += written; + + if (add_new_line) { + res = WriteFile(file, "\r\n", 2, &written, NULL); + print_result("WriteFile", written, true, ""); + if (!res) + throw_testWinFuncFailed("WriteFile"); + total += written; + } + } + + print_write_success(data, size, total); +} + +void TestW32Api::touch_file(const path& file_path, bool full_write_access) +{ + print_operation("Touching file", file_path); + + SYSTEMTIME st; + GetSystemTime(&st); + FILETIME ft; + if (!SystemTimeToFileTime(&st, &ft)) + throw_testWinFuncFailed("SystemTimeToFileTime"); + + auto share_all = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; + auto access = full_write_access ? GENERIC_WRITE : FILE_WRITE_ATTRIBUTES; + SafeHandle file(this, CreateFile(file_path.c_str(), access, share_all, NULL, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL)); + print_result("CreateFileW", 0, true, nullptr, true); + if (!file.valid()) + throw_testWinFuncFailed("CreateFile"); + + BOOL res = SetFileTime(file, nullptr, nullptr, &ft); + print_result("SetFileTime", res, true); + if (!res) + throw_testWinFuncFailed("SetFileTime"); +} + +void TestW32Api::delete_file(const path& file_path) +{ + print_operation("Deleting file", file_path); + + BOOL res = DeleteFileW(file_path.c_str()); + print_result("DeleteFileW", res, true); + if (!res) + throw_testWinFuncFailed("DeleteFileW"); +} + +void TestW32Api::copy_file(const path& source_path, const path& destination_path, + bool replace_existing) +{ + print_operation(replace_existing ? "Copy file over" : "Copy file", source_path, + destination_path); + + BOOL res = + CopyFileW(source_path.c_str(), destination_path.c_str(), !replace_existing); + print_result("CopyFileW", res, true); + if (!res) + throw_testWinFuncFailed("CopyFileW"); +} + +void TestW32Api::rename_file(const path& source_path, const path& destination_path, + bool replace_existing, bool allow_copy) +{ + print_operation(rename_operation_name(replace_existing, allow_copy), source_path, + destination_path); + + DWORD flags = 0; + if (replace_existing) + flags |= MOVEFILE_REPLACE_EXISTING; + if (allow_copy) + flags |= MOVEFILE_COPY_ALLOWED; + + BOOL res = MoveFileExW(source_path.c_str(), destination_path.c_str(), flags); + print_result("MoveFileExW", res, true); + if (!res) + throw_testWinFuncFailed("MoveFileExW"); +} diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.h b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.h new file mode 100644 index 0000000..cab8e54 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.h @@ -0,0 +1,36 @@ +#pragma once + +#include "test_filesystem.h" + +class TestW32Api : public TestFileSystem +{ +public: + TestW32Api(FILE* output) : TestFileSystem(output) {} + + path real_path(const char* abs_or_rel_path) override; + + FileInfoList list_directory(const path& directory_path) override; + + void create_path(const path& directory_path) override; + + void read_file(const path& file_path) override; + + void write_file(const path& file_path, const void* data, std::size_t size, + bool add_new_line, write_mode mode, bool rw_access = false) override; + + void touch_file(const path& file_path, bool full_write_access = false) override; + + void delete_file(const path& file_path) override; + + void copy_file(const path& source_path, const path& destination_path, + bool replace_existing) override; + + void rename_file(const path& source_path, const path& destination_path, + bool replace_existing, bool allow_copy) override; + + const char* id() override; + +private: + class SafeHandle; + class SafeFindHandle; +}; diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.cpp b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.cpp new file mode 100644 index 0000000..c5fb3a1 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.cpp @@ -0,0 +1,402 @@ +#include "usvfs_basic_test.h" + +const char* usvfs_basic_test::scenario_name() +{ + return SCENARIO_NAME; +} + +bool usvfs_basic_test::scenario_run() +{ + // Note: For regression purposes we don't really need to verify the results of most + // our operations as the usvfs_test_base postmortem_check will verify the final state. + // We still also verify the results here because: + // A. In some cases a later step may change the results. + // B. It is easier to understand and maintain the test when the important checks are + // together with + // their related operations. + // C. For open issues the verifications here serve as a "documentation" of the issue + // (i.e. not having + // proper copy_on_write, etc.). + + // Cases not covered by the test: + // - When a "temporary" virtualized directory is created (meaning the directory is + // created only to "shadow" + // a real directory) and is then emptied. This needs to be tested within the same + // prcoess and across different processes. + + ops_list(LR"(.)", true, true); + + // test proper path creation under overwrite when a virtualized folder is written to: + + verify_source_existance(LR"(overwrite\mfolder1)", false); + verify_source_existance(LR"(overwrite\mfolder2)", false); + verify_source_existance(LR"(overwrite\mfolder3)", false); + verify_source_existance(LR"(overwrite\mfolder4)", false); + + ops_overwrite(LR"(mfolder1\newfolder1\newfile1.txt)", + R"(newfile1.txt nonrecursive overwrite subfolder)", false); + verify_source_existance(LR"(overwrite\mfolder1\newfolder1\newfile1.txt)"); + ops_overwrite(LR"(mfolder1\newfile1.txt)", R"(newfile1.txt nonrecursive overwrite)", + false); + ops_read(LR"(mfolder1\newfile1.txt)"); + + ops_overwrite(LR"(mfolder2\newfile2.txt)", R"(newfile2.txt recursive overwrite)", + true); + ops_read(LR"(mfolder2\newfile2.txt)"); + verify_source_contents(LR"(overwrite\mfolder2\newfile2.txt)", + R"(newfile2.txt recursive overwrite)"); + ops_overwrite(LR"(mfolder2\newfile2.txt\fail)", + R"(newfile2.txt is a file so folder creation should fail)", true, + false); + verify_source_contents(LR"(overwrite\mfolder2\newfile2.txt)", + R"(newfile2.txt recursive overwrite)"); + ops_overwrite(LR"(mfolder2\mfile.txt\fail)", + R"(mfile.txt is a file so folder creation should fail)", true, false); + verify_source_existance(LR"(overwrite\mfolder2\mfile.txt)", false); + + ops_overwrite(LR"(mfolder3\newfolder3\newfile3.txt)", + R"(newfile3.txt recursive overwrite)", true); + ops_read(LR"(mfolder3\newfolder3\newfile3.txt)"); + verify_source_contents(LR"(overwrite\mfolder3\newfolder3\newfile3.txt)", + R"(newfile3.txt recursive overwrite)"); + // repeat mfolder3\newfolder3 test as that folder now exists in overwrite and that + // changes things + ops_overwrite(LR"(mfolder3\newfolder3\newfile3e.txt)", + R"(newfile3e.txt recursive overwrite)", true); + ops_read(LR"(mfolder3\newfolder3\newfile3e.txt)"); + verify_source_contents(LR"(overwrite\mfolder3\newfolder3\newfile3e.txt)", + R"(newfile3e.txt recursive overwrite)"); + + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4.txt)", + R"(newfile4.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4.txt)", + R"(newfile4.txt recursive overwrite)"); + // repeat mfolder4\newfolder4\d\e\e\p test as that folder now exists in overwrite and + // that changes things + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4e.txt)", + R"(newfile4e.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4e.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4e.txt)", + R"(newfile4e.txt recursive overwrite)"); + // and finally verify also non-recursive works + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)", + R"(newfile4enr.txt nonrecursive overwrite)", false); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)", + R"(newfile4enr.txt nonrecursive overwrite)"); + // finally check an intermediate folder: + ops_overwrite(LR"(mfolder4\newfolder4\d\e\epnewfile4r.txt)", + R"(epnewfile4r.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\epnewfile4r.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\epnewfile4r.txt)", + R"(epnewfile4r.txt recursive overwrite)"); + ops_overwrite(LR"(mfolder4\newfolder4\d\e\epnewfile4.txt)", + R"(epnewfile4.txt nonrecursive overwrite)", false); + ops_read(LR"(mfolder4\newfolder4\d\e\epnewfile4.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\epnewfile4.txt)", + R"(epnewfile4.txt nonrecursive overwrite)"); + + verify_mount_existance(LR"(rfolder\rcopyme4.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\newfolder4p)", false); + ops_copy(LR"(rfolder\rcopyme4.txt)", LR"(mfolder4\newfolder4p\rcopyme4.txt)", true); + verify_source_existance(LR"(overwrite\mfolder4\newfolder4p\rcopyme4.txt)", true); + verify_source_existance(LR"(mod4\mfolder4\mfile.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\mfile.txt)", false); + ops_copy(LR"(rfolder\rcopyme4.txt)", LR"(mfolder4\rcopyme4.txt)", false); + verify_source_existance(LR"(overwrite\mfolder4\rcopyme4.txt)"); + + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4.txt)", + R"(newfile4.txt recursive overwrite)"); + // repeat mfolder4\newfolder4\d\e\e\p test as that folder now exists in overwrite and + // that changes things + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4e.txt)", + R"(newfile4e.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4e.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4e.txt)", + R"(newfile4e.txt recursive overwrite)"); + // and finally verify also non-recursive works + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)", + R"(newfile4enr.txt nonrecursive overwrite)", false); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)", + R"(newfile4enr.txt nonrecursive overwrite)"); + // finally check an intermediate folder: + ops_overwrite(LR"(mfolder4\newfolder4\d\e\epnewfile4r.txt)", + R"(epnewfile4r.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\epnewfile4r.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\epnewfile4r.txt)", + R"(epnewfile4r.txt recursive overwrite)"); + ops_overwrite(LR"(mfolder4\newfolder4\d\e\epnewfile4.txt)", + R"(epnewfile4.txt nonrecursive overwrite)", false); + ops_read(LR"(mfolder4\newfolder4\d\e\epnewfile4.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\epnewfile4.txt)", + R"(epnewfile4.txt nonrecursive overwrite)"); + + // test copy on write/delete against source "mod": + + ops_touch(LR"(root0.txt)"); + verify_source_existance(LR"(overwrite\root0.txt)", false); + ops_touch(LR"(root1.txt)"); + verify_source_existance(LR"(overwrite\root1.txt)", false); + ops_touch(LR"(root2.txt)"); + verify_source_existance(LR"(overwrite\root1.txt)", false); + ops_touch(LR"(mod1.txt)"); + verify_source_existance(LR"(overwrite\mod1.txt)", false); + ops_touch(LR"(mfolder1\mfile.txt)"); + verify_source_existance(LR"(overwrite\mfolder1\mfile.txt)", false); + + ops_touch(LR"(root0w.txt)", true); + verify_source_existance(LR"(overwrite\root0w.txt)", false); + ops_touch(LR"(root1w.txt)", true); + verify_source_existance(LR"(overwrite\root1w.txt)", false); + ops_touch(LR"(root2w.txt)", true); + verify_source_existance(LR"(overwrite\root1w.txt)", false); + ops_touch(LR"(mod1w.txt)", true); + verify_source_existance(LR"(overwrite\mod1w.txt)", false); + ops_touch(LR"(mfolder1\mfilew.txt)", true); + verify_source_existance(LR"(overwrite\mfolder1\mfilew.txt)", false); + + { + const auto& old_contents = source_contents(LR"(mod4\mfolder4\mfileoverwrite.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfileoverwrite.txt)", + old_contents.c_str()); + ops_overwrite(LR"(mfolder4\mfileoverwrite.txt)", + R"(mfolder4\mfileoverwrite.txt overwrite)", false); + ops_read(LR"(mfolder4\mfileoverwrite.txt)"); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(mod4\mfolder4\mfileoverwrite.txt)", + old_contents.c_str()); + verify_source_contents(LR"(overwrite\mfolder4\mfileoverwrite.txt)", + R"(mfolder4\mfileoverwrite.txt overwrite)"); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfileoverwrite.txt)", + R"(mfolder4\mfileoverwrite.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfileoverwrite.txt)", false); + } + } + + { + const auto& old_contents = source_contents(LR"(mod4\mfolder4\mfilerewrite.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfilerewrite.txt)", old_contents.c_str()); + ops_rewrite(LR"(mfolder4\mfilerewrite.txt)", + R"(mfolder4\mfilerewrite.txt rewrite)"); + ops_read(LR"(mfolder4\mfilerewrite.txt)"); + if (auto copy_on_readwrite_implemented = false) { + verify_source_contents(LR"(mod4\mfolder4\mfilerewrite.txt)", + old_contents.c_str()); + verify_source_contents(LR"(overwrite\mfolder4\mfilerewrite.txt)", + R"(mfolder4\mfilerewrite.txt rewrite)"); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfilerewrite.txt)", + R"(mfolder4\mfilerewrite.txt rewrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfilerewrite.txt)", false); + } + } + + { + const auto& old_contents = source_contents(LR"(mod4\mfolder4\mfilemoveover.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfilemoveover.txt)", old_contents.c_str()); + ops_overwrite(LR"(mfolder4\temp_mfilemoveover.txt)", + R"(mfolder4\mfilemoveover.txt overwrite)", false); + verify_source_contents(LR"(overwrite\mfolder4\temp_mfilemoveover.txt)", + R"(mfolder4\mfilemoveover.txt overwrite)"); + ops_rename(LR"(mfolder4\temp_mfilemoveover.txt)", LR"(mfolder4\mfilemoveover.txt)", + true); + ops_read(LR"(mfolder4\mfilemoveover.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\temp_mfilemoveover.txt)", false); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(mod4\mfolder4\mfilemoveover.txt)", + old_contents.c_str()); + verify_source_contents(LR"(overwrite\mfolder4\mfilemoveover.txt)", + R"(mfolder4\mfilemoveover.txt overwrite)"); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfilemoveover.txt)", + R"(mfolder4\mfilemoveover.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfilemoveover.txt)", false); + } + } + + { + const auto& old_contents = + source_contents(LR"(mod4\mfolder4\mfiledeletewrite.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite.txt)", + old_contents.c_str()); + ops_deleteoverwrite(LR"(mfolder4\mfiledeletewrite.txt)", + R"(mfolder4\mfiledeletewrite.txt overwrite)", false); + ops_read(LR"(mfolder4\mfiledeletewrite.txt)"); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(overwrite\mfolder4\mfiledeletewrite.txt)", + R"(mfolder4\mfiledeletewrite.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite.txt)", + old_contents.c_str()); + else + verify_source_existance(LR"(mod4\mfolder4\mfiledeletewrite.txt)", false); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite.txt)", + R"(mfolder4\mfiledeletewrite.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfiledeletewrite.txt)", false); + } + } + + { + const auto& old_contents = + source_contents(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)", + old_contents.c_str()); + ops_delete(LR"(mfolder4\mfiledeletewrite2p.txt)"); + ops_overwrite(LR"(mfolder4\mfiledeletewrite2p.txt)", + R"(mfolder4\mfiledeletewrite2p.txt overwrite)", false); + ops_read(LR"(mfolder4\mfiledeletewrite2p.txt)"); + if (bool protect_virtualized_or_track_deleted_only_in_current_process = true) { + verify_source_contents(LR"(overwrite\mfolder4\mfiledeletewrite2p.txt)", + R"(mfolder4\mfiledeletewrite2p.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)", + old_contents.c_str()); + else + verify_source_existance(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)", false); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)", + R"(mfolder4\mfiledeletewrite2p.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfiledeletewrite2p.txt)", false); + } + } + + { + const auto& old_contents = source_contents(LR"(mod4\mfolder4\mfiledeletemove.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove.txt)", + old_contents.c_str()); + ops_overwrite(LR"(mfolder4\temp_mfiledeletemove.txt)", + R"(mfolder4\mfiledeletemove.txt overwrite)", false); + verify_source_contents(LR"(overwrite\mfolder4\temp_mfiledeletemove.txt)", + R"(mfolder4\mfiledeletemove.txt overwrite)"); + ops_deleterename(LR"(mfolder4\temp_mfiledeletemove.txt)", + LR"(mfolder4\mfiledeletemove.txt)"); + ops_read(LR"(mfolder4\mfiledeletemove.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\temp_mfiledeletemove.txt)", false); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(overwrite\mfolder4\mfiledeletemove.txt)", + R"(mfolder4\mfiledeletemove.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove.txt)", + old_contents.c_str()); + else + verify_source_existance(LR"(mod4\mfolder4\mfiledeletemove.txt)", false); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove.txt)", + R"(mfolder4\mfiledeletemove.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfiledeletemove.txt)", false); + } + } + + { + const auto& old_contents = + source_contents(LR"(mod4\mfolder4\mfiledeletemove2p.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove2p.txt)", + old_contents.c_str()); + ops_delete(LR"(mfolder4\mfiledeletemove2p.txt)"); + ops_overwrite(LR"(mfolder4\temp_mfiledeletemove2p.txt)", + R"(mfolder4\mfiledeletemove2p.txt overwrite)", false); + verify_source_contents(LR"(overwrite\mfolder4\temp_mfiledeletemove2p.txt)", + R"(mfolder4\mfiledeletemove2p.txt overwrite)"); + ops_rename(LR"(mfolder4\temp_mfiledeletemove2p.txt)", + LR"(mfolder4\mfiledeletemove2p.txt)", false); + ops_read(LR"(mfolder4\mfiledeletemove2p.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\temp_mfiledeletemove2p.txt)", false); + if (bool protect_virtualized_or_track_deleted_only_in_current_process = true) { + verify_source_contents(LR"(overwrite\mfolder4\mfiledeletemove2p.txt)", + R"(mfolder4\mfiledeletemove2p.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove2p.txt)", + old_contents.c_str()); + else + verify_source_existance(LR"(mod4\mfolder4\mfiledeletemove2p.txt)", false); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove2p.txt)", + R"(mfolder4\mfiledeletemove2p.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfiledeletemove2p.txt)", false); + } + } + + // test copy on write/delete/move against original mount files: + + { + const auto& old_contents = mount_contents(LR"(rfolder\rfilerewrite.txt)"); + ops_rewrite(LR"(rfolder\rfilerewrite.txt)", R"(rfolder\rfilerewrite.txt rewrite)"); + ops_read(LR"(rfolder\rfilerewrite.txt)"); + if (auto copy_on_readwrite_implemented = false) { + verify_mount_contents(LR"(rfolder\rfilerewrite.txt)", old_contents.c_str()); + verify_source_contents(LR"(overwrite\rfolder\rfilerewrite.txt)", + R"(rfolder\rfilerewrite.txt rewrite)"); + } else { + verify_mount_contents(LR"(rfolder\rfilerewrite.txt)", + R"(rfolder\rfilerewrite.txt rewrite)"); + verify_source_existance(LR"(overwrite\rfolder\rfilerewrite.txt)", false); + } + } + ops_overwrite(LR"(rfolder\rfilerewrite.txt\fail)", + R"(rfilerewrite.txt is a file so folder creation should fail)", true, + false); + verify_mount_existance( + LR"(rfolder\rfilerewrite.txt)"); // verifies its a file and not a directory + if (auto copy_on_readwrite_implemented = false) + verify_source_existance( + LR"(overwrite\rfolder\rfilerewrite.txt)"); // verifies its a file and not a + // directory + else + verify_source_existance(LR"(overwrite\rfolder\rfilerewrite.txt)", false); + ops_overwrite(LR"(rfolder\rfile0.txt\fail)", + R"(rfile0.txt is a file so folder creation should fail)", true, false); + verify_mount_existance( + LR"(rfolder\rfile0.txt)"); // verifies its a file and not a directory + + { + const auto& old_contents = mount_contents(LR"(rfolder\rfiledeletewrite.txt)"); + ops_deleteoverwrite(LR"(rfolder\rfiledeletewrite.txt)", + R"(rfolder\rfiledeletewrite.txt overwrite)", false); + ops_read(LR"(rfolder\rfiledeletewrite.txt)"); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(overwrite\rfolder\rfiledeletewrite.txt)", + R"(rfolder\rfiledeletewrite.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_mount_contents(LR"(rfolder\rfiledeletewrite.txt)", old_contents.c_str()); + else + verify_mount_existance(LR"(rfolder\rfiledeletewrite.txt)", false); + } else { + verify_mount_contents(LR"(rfolder\rfiledeletewrite.txt)", + R"(rfolder\rfiledeletewrite.txt overwrite)"); + verify_source_existance(LR"(overwrite\rfolder\rfiledeletewrite.txt)", false); + } + } + + { + const auto& old_contents = mount_contents(LR"(rfolder\rfiledelete.txt)"); + ops_delete(LR"(rfolder\rfiledelete.txt)"); + ops_read(LR"(rfolder\rfiledelete.txt)", false); + if (auto proper_delete_implemented = false) + verify_mount_contents(LR"(rfolder\rfiledelete.txt)", old_contents.c_str()); + else + verify_mount_existance(LR"(rfolder\rfiledelete.txt)", false); + } + + { + const auto& old_contents = mount_contents(LR"(rfolder\rfileoldname.txt)"); + ops_rename(LR"(rfolder\rfileoldname.txt)", LR"(rfolder\rfilenewname.txt)", false, + false); + ops_read(LR"(rfolder\rfileoldname.txt)", false); + ops_read(LR"(rfolder\rfilenewname.txt)"); + verify_source_contents(LR"(overwrite\rfolder\rfilenewname.txt)", + old_contents.c_str()); + verify_mount_existance(LR"(rfolder\rfilenewname.txt)", false); + if (auto copy_on_move_implemented = false) + verify_mount_contents(LR"(rfolder\rfileoldname.txt)", old_contents.c_str()); + else + verify_mount_existance(LR"(rfolder\rfileoldname.txt)", false); + } + + ops_list(LR"(.)", true, true); + + return true; +} diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.h b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.h new file mode 100644 index 0000000..bec81f2 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.h @@ -0,0 +1,14 @@ +#pragma once + +#include "usvfs_test_base.h" + +class usvfs_basic_test : public usvfs_test_base +{ +public: + static constexpr auto SCENARIO_NAME = "basic"; + + usvfs_basic_test(const usvfs_test_options& options) : usvfs_test_base(options) {} + + virtual const char* scenario_name(); + virtual bool scenario_run(); +}; diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test.cpp b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test.cpp new file mode 100644 index 0000000..e6e6bb1 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test.cpp @@ -0,0 +1,242 @@ + +#include +#include + +#include +#include +#include + +#include "usvfs_basic_test.h" +#include + +void print_usage(const std::wstring& exe_name, const std::wstring& test_name) +{ + using namespace std; + wcerr << "usage: " << exe_name << " [] [: